Using Clarity Maps for On‑Chain State: A Practical Guide
Clarity maps give you typed, persistent key‑value storage on Stacks. This post walks through a real‑world example, shows the costs, and explains when to batch writes or avoid deep nesting.
18 May 2026, 11:30 UTC

The problem: persistent key‑value storage on Stacks
Smart contracts on the Stacks blockchain need a way to keep data between calls. Global variables disappear after each transaction, so developers reach for a durable, typed store. Clarity solves this with a first‑class map type that lives in the contract’s state and survives across calls.
Why maps are the right tool
A Clarity map is an associative array declared with define-map. The key and value types are fixed at compile time, giving you static guarantees that you never mix principals with integers, for example. The runtime fetches only the requested key, so look‑ups are O(1) and gas costs stay predictable. Writes (map-insert, map-set, map-delete) are atomic and persisted on‑chain, but each write costs a Stacks transaction fee and counts toward the block’s 1 MB state‑size limit.
Worked example: a simple token‑balance ledger
The contract below shows a minimal balance map. It demonstrates declaration, insertion, reading, and deletion.
(define-contract token-ledger
(define-map balances principal uint)
;; Public function to mint tokens to a principal
(define-public (mint (to principal) (amount uint))
(begin
(map-insert balances to amount)
(ok true)))
;; Read a balance – returns 0 if the key is absent
(define-read-only (get-balance (who principal))
(default-to u0 (map-get? balances who)))
;; Burn tokens by removing the entry
(define-public (burn (from principal))
(begin
(map-delete balances from)
(ok true))))
Where to run this: paste the code into the Clarity Playground or save it as token-ledger.clar and deploy with the Clarity CLI (clarinet deploy) against a local devnet. No special permissions are required beyond a Stacks wallet for testnet/mainnet deployments.
Expected checks:
- Call
mintwith a principal and amount – the transaction should succeed and the map entry appears. - Call
get-balancefor the same principal – it returns the minted amount. - Call
burn– the entry is removed and a subsequentget-balancereturns 0.
Risks: each mint or burn incurs a fee (≈0.001 STX on testnet). If you mint thousands of entries in a single block you may hit the 1 MB state limit and the transaction will be rejected.
Trade‑offs and limitations
| Aspect | Benefit | Cost / Limitation |
|---|---|---|
| Typed keys & values | Compile‑time safety, no accidental mixing | Heterogeneous keys require separate maps |
| Atomic updates | Consistent state, no partial writes | Every write pays a transaction fee |
| O(1) look‑ups | Predictable gas, only requested key loaded | Cannot iterate over all keys on‑chain |
| Nesting maps | Enables complex structures (e.g., map of maps) | Deep nesting inflates contract state, risking the 1 MB/block ceiling |
Because the entire map is not loaded into memory, you avoid the “full‑scan” gas spikes seen in some other languages. However, you also lose the ability to enumerate keys directly from the contract; off‑chain indexing (via Stacks API or a subgraph) is the usual workaround.
Actionable next steps
- Prototype in the Playground – paste the example, run the three calls, and observe the persisted state across calls.
- Measure gas – use
clarinet testwith the--costsflag to see the exact fee formap-insertvs.map-get?. - Stress‑test on testnet – write a script that inserts 5 000 entries in a loop, then monitor the transaction receipt for
block-size-exceedederrors. This validates the practical ceiling for your design. - Plan for batching – if your app needs high‑frequency updates, aggregate changes off‑chain and commit a single
map-insertper block, or use a separate “delta” map that you reconcile periodically.
By keeping maps flat, typing keys strictly, and batching writes, you get reliable on‑chain state without surprise fees or rejected blocks.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.