Learn how Sui decides who owns what — and how to move, share, or lock things down. We'll cover owned, shared, and immutable objects, plus transfer functions, custom transfer policies, and more.
Ownership on Sui works a lot like ownership in real life. Your phone is yours — only you can use it. A park bench is shared — anyone can sit on it. A statue in a museum is frozen — everyone can look but nobody can change it. Every object on Sui is in exactly one of these three states at any time: **Owned objects** belong to a single address. Only that person can use them in transactions. Think of these as your personal belongings. **Shared objects** are accessible by anyone. Any transaction can read or modify them, but this requires the network to agree on what order things happen (we call this "consensus"). Think of these as public resources. **Immutable objects** are frozen forever. Anyone can look at them, but nobody can change, delete, or move them. Think of these as things behind glass in a museum. Knowing when to use each type is a core Sui design skill. Owned objects are fast (no waiting). Shared objects let people collaborate (like a marketplace). Immutable objects give you trustworthy reference data that can never be tampered with.
Here's something cool about Sui: when you use your own stuff (owned objects), transactions are lightning fast because there's no waiting in line. Shared objects need everyone to agree on the order, which takes a tiny bit longer. Let's break that down: When a transaction only touches **owned objects**, Sui can process it without going through consensus at all. That's right — no waiting for the whole network to agree. This is why Sui can achieve sub-second finality for things like transferring your NFT or updating your profile. When a transaction touches at least one **shared object**, it needs to go through consensus (a process called Mysticeti) so that everyone agrees on the order. This is still fast, but it's a bit slower than owned-only transactions. **Immutable objects** are the best of both worlds — any transaction can read them without adding any consensus overhead, because they never change. No conflicts possible! The takeaway for you as a developer: use owned objects for personal stuff (user profiles, inventory items), shared objects only when multiple people truly need to read and write the same state (like a DEX pool), and immutable objects for reference data that should never change.
How you hand an object to a function matters a lot. It's like the difference between giving someone your car versus lending them the keys. There are four ways to pass an object to a function: **By value** (`obj: MyStruct`) — you're giving away the object entirely. The function takes full ownership, and you lose it. Inside the function, the object must be explicitly transferred to someone, shared, frozen, or taken apart (destructured). If you forget to handle it, the compiler will yell at you. **By mutable reference** (`obj: &mut MyStruct`) — you're lending it with permission to make changes. The function can read and modify fields, but you still own the object when the function is done. Like lending someone your car keys with permission to adjust the mirrors. **By immutable reference** (`obj: &MyStruct`) — read-only access. The function can look but can't touch. Like showing someone a painting through a window. **Receiving** (`obj: Receiving<MyStruct>`) — a special pattern for objects that were sent to another object (not directly to you). We'll cover this in detail shortly. A common beginner mistake is taking an object by value when you only needed a reference — and then wondering where the object went! If you just need to read or modify, use a reference.
Think of it this way: `transfer::transfer` is like a private courier that only the creator can use. `transfer::public_transfer` is like the postal service — anyone can send the package. Here's the detail: `transfer::transfer` can move any object that has the `key` ability, but it can **only be called inside the module that defines the struct**. This is the creator keeping control. Nobody else can move your object unless you let them. `transfer::public_transfer` works on objects that have **both `key` and `store`** abilities, and it can be called from **any module** — including transaction blocks that users build from their wallets. This is the permissionless, open-for-everyone version. The same pattern applies to sharing and freezing: - `transfer::share_object` vs `transfer::public_share_object` - `transfer::freeze_object` vs `transfer::public_freeze_object` So here's the design decision you'll face with every struct: if you want users to freely trade your object (like a standard NFT), give it `store`. If you want to control every transfer (like enforcing royalties), leave off `store` and expose your own transfer function that calls `transfer::transfer` internally. We'll see a real example of that pattern in a bit.
Sharing is permanent — once you put something in the public square, you can't take it back. Freezing is like putting something in a museum display case — everyone can see it, but nobody can touch it. Let's look at each one: **Sharing** makes an object accessible to everyone. Any transaction from any address can read or modify it. You share an object by calling `transfer::share_object(obj)` inside the defining module, or `transfer::public_share_object(obj)` for objects with `store`. Shared objects are perfect for global state — things like liquidity pools, auction contracts, or game worlds that many people interact with. But here's the important part: sharing is a **one-way door**. Once an object is shared, it can never become owned again. Ever. So think carefully before sharing! **Freezing** makes an object permanently immutable. Once frozen, nobody can change it, delete it, or move it — not even you. You freeze with `transfer::freeze_object(obj)` or `transfer::public_freeze_object(obj)`. Frozen objects are great for published configuration, metadata, or reference data that should be trustworthy forever. Both sharing and freezing take the object by value — meaning you need to own it first, and afterward you don't hold it anymore. You're giving it to the network.
Object-to-object transfer is like getting mail delivered to your house. Someone sends a package to your "mailbox" object, and you pick it up when you're ready. Here's how it works: instead of transferring an object to a wallet address, you transfer it to another object's address using `transfer::transfer(child, object::id_to_address(&parent_id))`. The child object is now "in the mailbox" of the parent object. The parent's owner can then claim it using `transfer::receive<T>(&mut parent.id, receiving_ticket)`. The `Receiving<T>` type is a special ticket that proves "hey, this object was sent to your parent — here's your claim stub." This pattern is super useful for inbox-style designs. Imagine a user's profile object that can receive items, messages, or tokens — even when the user is offline. They log in later and pick up everything that was sent to them. A few rules to remember: 1. You need a mutable reference to the parent's UID to call `receive` — this proves you own the parent. 2. The received object comes back by value, so you need to do something with it (transfer it, use it, etc.). 3. Only the parent's owner can initiate the receive — nobody else can open your mailbox.
By leaving off the `store` ability, you become the gatekeeper of all transfers. Want to charge a royalty? Enforce a cooldown? Require an allowlist? This is how. Here's the pattern: when your struct has only `key` (no `store`), nobody can use `public_transfer` on it. The only way to move the object is through functions that YOU define in your module — functions that internally call `transfer::transfer`. This is huge for things like NFT royalties. Without `store`, you can write a `transfer_with_royalty` function that collects a fee every time the NFT changes hands. Nobody can bypass your function because `public_transfer` simply won't work on your struct. The Sui ecosystem has even formalized this with the Kiosk framework, which uses `TransferPolicy<T>` objects to define rules (royalty percentages, lock-up periods, allowlists) that must be satisfied before a transfer goes through. The recipe is simple: 1. Define your struct with `key` only — no `store`. 2. Write a custom function that checks your conditions. 3. Call `transfer::transfer` inside that function. 4. Users can't go around you because `public_transfer` requires `store`.
**Wrapping** is like putting an object inside a box and closing the lid. Once wrapped, the inner object disappears from the outside world — it's no longer independently accessible. It becomes part of the wrapper object. For an object to be wrappable (stored as a field inside another struct), it needs the `store` ability. When you later unpack (destructure) the wrapper, the inner object reappears and you need to do something with it — transfer it, share it, freeze it, or destroy it. Why is this useful? Think about escrow. You wrap an NFT inside an Escrow object, and the NFT is locked until the escrow conditions are met. Nobody can access the NFT directly — it's sealed inside the box. When conditions are fulfilled, you unwrap and release it. Wrapping is also the foundation of dynamic fields and dynamic object fields, where objects are stored under dynamic keys on a parent object. Under the hood, Sui is wrapping them for you.
When you're designing a Sui module, every object needs an ownership strategy. Here's a simple decision framework to help you choose: **Use owned objects** when the state belongs to one person and doesn't need to be shared. Examples: user profiles, inventory items, personal settings. Why: maximum speed, no consensus overhead, transactions fly through. **Use shared objects** when multiple people need to read and write the same state. Examples: AMM liquidity pools, auction contracts, multiplayer game worlds. The trade-off: every transaction goes through consensus, which is a bit slower. **Use immutable objects** for data that should never change once created. Examples: published artwork metadata, protocol parameters locked at launch, package info. Why: zero-cost reads from any transaction — and everyone can trust the data hasn't been tampered with. **Use wrapping** when an object should be temporarily inaccessible. Examples: escrowed assets, staked tokens, items in a crafting recipe. The object is locked inside another object until conditions are met. **Omit `store`** when you need to control every transfer (royalties, cooldowns, allowlists). **Include `store`** when you want maximum composability and want users to freely trade your objects. Don't worry about memorizing all of this — you'll develop intuition as you build more. The important thing is knowing these options exist.