Time to learn how to manage groups of data in Sui Move — lists, key-value maps, and fields you can add on the fly. No prior experience with data structures needed — we'll explain everything from scratch.
A vector is just a list — like a shopping list or a playlist. You can add items, remove items, and check what's in it. In Move, a vector is written as `vector<T>` where T is the type of thing in the list. So `vector<u64>` is a list of numbers, and `vector<String>` is a list of text. Here's the thing: vectors live inside your objects — they're not standalone things on the blockchain. They're perfect for small, bounded collections, like a list of tags (maybe 5-10 items), a set of high scores (top 10), or a short list of wallet addresses. The `vector` module from the standard library gives you all the tools you need to create lists, add items, remove items, peek at items, and search through them. Let's see it in action:
Here's your toolbox for working with lists — push to add, pop to remove from the end, borrow to peek at an item. Let's go through every operation you'll use: - `vector::empty<T>()` -- Create a brand new empty list - `vector::push_back(&mut v, elem)` -- Add an item to the end of the list - `vector::pop_back(&mut v)` -- Remove and return the last item - `vector::length(&v)` -- How many items are in the list? - `vector::borrow(&v, index)` -- Peek at the item at position `index` (read-only) - `vector::borrow_mut(&mut v, index)` -- Get a mutable reference to change the item at position `index` - `vector::contains(&v, &elem)` -- Is this item in the list? (returns true/false) - `vector::index_of(&v, &elem)` -- Where is this item? (returns (found: bool, position: u64)) - `vector::remove(&mut v, index)` -- Remove at position, sliding everything after it left (keeps order) - `vector::swap_remove(&mut v, index)` -- Remove by swapping with the last item, then popping (faster but scrambles order) Here's a common beginner question: "When should I use `remove` vs `swap_remove`?" Use `remove` when the order of your list matters (like a leaderboard). Use `swap_remove` when it doesn't (like a bag of items) — it's much faster for large lists because it doesn't need to shift everything around.
Good news — Move gives you a shortcut for creating vectors with values already in them: just write `vector[1, 2, 3]`. No need to create an empty vector and push items one by one. You can also put vectors inside your structs. For example, a Player object might have a `badges: vector<String>` field to track their achievements. When the struct is stored on-chain, the vector goes with it — it's all bundled together as one object. Here's a really important thing to keep in mind: because the vector lives inside the object, the *entire* vector gets loaded every time you access that object. For a list of 5 badges? No problem. For a list of 10,000 items? That's going to be slow and expensive. If your list might grow really large, you'll want to use a Table instead (we'll cover that next). A good rule of thumb: vectors are great for lists under ~100 items. Above that, reach for a Table.
When your list gets huge — thousands of items — a vector gets slow because it loads everything at once. A Table is like a filing cabinet: you only pull out the folder you need. More precisely, `Table<K, V>` from the `sui::table` module is a key-value store. You look things up by key (like an address or a name) and get back the associated value. Under the hood, each entry is stored separately on the blockchain, so accessing one entry doesn't load all the others. A few things to know about Table keys and values: - Keys must have `copy + drop + store` abilities (common types like `address`, `u64`, and `String` all work) - Values must have the `store` ability - Tables themselves have `key + store`, so they can live inside other objects Use a Table when you need a map-like structure that could grow to hundreds or thousands of entries — like a registry of usernames, a token balance ledger, or a list of game scores.
Sui also has `ObjectTable<K, V>` — same as a Table, but the things you store stay visible to blockchain explorers. Think of it as a glass display case vs an opaque storage box. With a regular Table, stored items are hidden from the outside world. With an ObjectTable, they remain visible and queryable. The catch: ObjectTable values must be Sui objects (they need `key + store` abilities). Regular Table values just need `store`. Both Table and ObjectTable share the exact same API: - `table::new(ctx)` / `object_table::new(ctx)` -- Create an empty collection - `table::add(&mut t, key, value)` -- Insert a pair (crashes if key already exists!) - `table::borrow(&t, key)` -- Peek at the value (read-only) - `table::borrow_mut(&mut t, key)` -- Get a mutable reference to change the value - `table::remove(&mut t, key)` -- Remove and return the value - `table::contains(&t, key)` -- Does this key exist? (safe check) - `table::length(&t)` -- How many entries? - `table::is_empty(&t)` -- Any entries at all? So when do you pick which? Use ObjectTable when you want stored objects to show up in block explorers (like NFTs in a marketplace). Use Table when you want entries to be private, or when your values aren't Sui objects.
Let's see — you now know about vectors, Tables, and ObjectTables. How do you decide which one to use? Here's a practical guide: **vector<T>** -- Your go-to for small, bounded lists. Like a player's top 10 scores, a list of badge names, or a short array of config values. Everything loads together, which is fine when the list is small. You can iterate through it on-chain. **Table<K, V>** -- Reach for this when your collection could grow large or you're not sure how big it'll get. It's a key-value map where you look things up by key. Only the entries you access cost gas. The downside: you can't iterate through all entries on-chain, and entries are hidden from explorers. **ObjectTable<K, V>** -- Same performance as Table, but the objects you store remain visible to explorers and indexers. Use this when discoverability matters — like a marketplace of NFTs or a registry of game characters. **Dynamic Fields** -- The most flexible option of all. We'll cover these next! They let you attach arbitrary data to any object at runtime. Tables are actually built on top of dynamic fields under the hood.
Imagine you could add new pockets to a backpack whenever you want. Dynamic fields let you attach new data to any object at any time — even after you've published your code. Here's why that's powerful: normally, when you define a struct, its fields are locked in at compile time. A `Character` has a `name` and `health` and that's it. But with dynamic fields, you can bolt on new fields later — a "level" field, an "inventory" field, a "guild_name" field — without changing the original struct definition. The `sui::dynamic_field` module (usually imported as `field`) gives you these tools: - `field::add<Name, Value>(&mut obj_uid, name, value)` -- Attach a new field - `field::borrow<Name, Value>(&obj_uid, name)` -- Peek at the field value (read-only) - `field::borrow_mut<Name, Value>(&mut obj_uid, name)` -- Get a mutable reference to change it - `field::remove<Name, Value>(&mut obj_uid, name)` -- Remove the field and get the value back - `field::exists_<Name>(&obj_uid, name)` -- Does this field exist? The Name type (the key) needs `copy + drop + store`. The Value type needs `store`. And every name must be unique on an object — trying to add a duplicate name will crash your transaction.
Same idea as dynamic fields, but the objects you attach keep their identity — they're still findable and browsable, not hidden away. Let's unpack that. When you store an object using regular `dynamic_field`, the object gets "wrapped" — it disappears from block explorers and can't be found by its ID anymore. It's like putting something in a sealed box. With `dynamic_object_field` (imported as `ofield`), the attached object stays visible — like putting it in a glass display case. The trade-off: dynamic_object_field values *must* be Sui objects (they need `key + store`). Regular dynamic fields can store any value with `store`. The API is identical to dynamic_field: - `ofield::add<Name, Value>(&mut uid, name, value)` -- Attach a child object (stays visible) - `ofield::borrow<Name, Value>(&uid, name)` -- Peek at it (read-only) - `ofield::borrow_mut<Name, Value>(&mut uid, name)` -- Get a mutable reference - `ofield::remove<Name, Value>(&mut uid, name)` -- Detach and return the child object - `ofield::exists_<Name>(&uid, name)` -- Does this child exist? Use dynamic object fields when you want child objects to be discoverable — like NFTs inside a collection, or items in a marketplace listing.
Now that you understand the building blocks, let's see the patterns that real Sui developers use with dynamic fields: **Extension Pattern**: Ship a basic object now, then add new features later without rewriting your module. Published a Character with just a name? Add a "level" field in your next upgrade. Your users' existing Characters get the new field without migration. **Heterogeneous Map**: Store different types of data on the same object. A Table forces all values to be the same type — dynamic fields don't. Mix numbers, strings, and custom structs freely. **Namespace Pattern**: Use a custom struct as the Name type to prevent collisions. If two modules both try to add a field called "level" to the same object, they'd clash. But if each module uses its own struct type as the key, they can't interfere with each other. **Lazy Initialization**: Only attach fields when they're actually needed. Why create empty "guild" and "inventory" fields for every Character when most players might never use them? Add them on demand and save gas. Fun fact: Tables are actually built on top of dynamic fields! When you call `table::add`, it internally calls `field::add`. Tables just give you a cleaner API for the common case of a uniform key-value map.