Near Learn

Pick the right collection

Pick NEAR SDK store collections for gas: LookupMap vs IterableMap vs Vector, why a Vec or HashMap in contract state burns gas, and unique storage prefixes.

Intermediate6 min read3-question check

Your contract struct is stored as a single record. On every call near-sdk reads that record and Borsh-deserializes the whole struct; on every change call it serializes and writes it back. Anything you put directly in the struct is paid for on every call, whether the method uses it or not.

The collections in near_sdk::store avoid that: the struct holds only a small handle (a storage prefix, plus a length for the iterable ones), and each entry lives under its own storage key that is read only when you ask for it.

The anti-pattern: native collections in state#

Avoid: the whole HashMap is loaded and saved on every call
Rust
use near_sdk::{near, AccountId};
use std::collections::HashMap;

#[near(contract_state)]
#[derive(Default)]
pub struct Bad {
    // 10,000 accounts here means 10,000 entries deserialized
    // just to read one balance, and all of them rewritten on every change
    balances: HashMap<AccountId, u128>,
}
Prefer: store collections with a BorshStorageKey enum for prefixes
Rust
use near_sdk::store::{IterableSet, LookupMap, Vector};
use near_sdk::{near, AccountId, BorshStorageKey};

#[near(serializers = [borsh])]
#[derive(BorshStorageKey)]
enum StorageKey {
    Balances,
    Members,
    History,
}

#[near(contract_state)]
pub struct Good {
    balances: LookupMap<AccountId, u128>, // read one key, load one entry
    members: IterableSet<AccountId>,      // can be listed and paginated
    history: Vector<String>,              // append-only, read by index
}

impl Default for Good {
    fn default() -> Self {
        Self {
            balances: LookupMap::new(StorageKey::Balances),
            members: IterableSet::new(StorageKey::Members),
            history: Vector::new(StorageKey::History),
        }
    }
}

Which one to use#

CollectionIterable?What a call loadsUse when
LookupMap<K, V>NoOnly the keys you touchBalances and per-account data looked up by key. The cheapest map.
LookupSet<T>NoOnly the values you checkAllow-lists, "already claimed" flags
IterableMap<K, V>YesKeys you touch; iteration walks entriesYou must list or paginate entries. Costs extra storage per entry for its key index.
IterableSet<T>YesValues you touch; iteration walks entriesMembership you must enumerate
Vector<T>Yes, by indexOnly the indexes you readOrdered lists, append-only logs, queues
UnorderedMap / UnorderedSetYesSame as the iterable versionsLegacy: deprecated in near-sdk 5 in favour of IterableMap / IterableSet
Vec / HashMap in the structYesEverything, every callOnly small, bounded data such as a handful of admin accounts
near_sdk::store collections (near-sdk 5.x)

Unique storage prefixes#

Every entry is stored under prefix + key, so two collections with the same prefix read and overwrite each other’s data. A #[derive(BorshStorageKey)] enum gives each collection a distinct prefix that Borsh serializes to a single byte (the variant index), which also keeps every key short.

For nested collections, such as one set of tokens per owner, put the owner into the prefix with a variant that carries data, for example StorageKey::TokensPerOwner { owner_hash: env::sha256(owner.as_bytes()) }. Every inner collection then gets its own key space.

Check yourself

3 questions · progress saved in this browser

  1. 1.Why does a large HashMap stored directly in the contract struct cost so much gas?
  2. 2.You store a balance per account. The contract only ever reads one balance at a time and never lists them. Which collection fits best?
  3. 3.An upgrade moves a new variant to the top of your BorshStorageKey enum. What happens?