What a mapping is

A mapping associates one type with another:

mapping(address => uint256) public balances;

In this example, each address key maps to a uint256 value. The compiler stores the data efficiently in contract storage, and you can read or write values using bracket notation:

balances[msg.sender] = 100;
uint256 current = balances[msg.sender];

Mappings are commonly used when:

  • each user needs a separate balance
  • access control depends on address-based permissions
  • you want to store per-asset or per-ID metadata
  • you need a lookup table for configuration values

Why mappings are useful

Mappings solve a common blockchain problem: many contracts need to associate arbitrary identifiers with state, but storage is expensive and iteration is limited. A mapping gives you direct access without scanning a list.

Typical use cases

Use caseExample keyExample value
Token balancesaddressuint256
Whitelist statusaddressbool
Role membershipaddressbool or uint8
Order statebytes32struct
Per-user preferencesaddressstruct

A mapping is especially useful when you do not need to enumerate all entries on-chain. If you only need to check whether a key exists or read its value, mappings are a natural fit.

Declaring and using a mapping

Here is a minimal contract that stores deposits by address:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract DepositLedger {
    mapping(address => uint256) public deposits;

    function deposit() external payable {
        deposits[msg.sender] += msg.value;
    }

    function getDeposit(address user) external view returns (uint256) {
        return deposits[user];
    }
}

Key points

  • mapping(address => uint256) declares the key and value types.
  • public automatically generates a getter for simple mappings.
  • msg.sender is the caller’s address, which is a common mapping key.
  • += updates the stored value in place.

This pattern is common in escrow, staking, and accounting contracts.

Default values and missing keys

One of the most important mapping behaviors is that reading a missing key does not revert. Instead, Solidity returns the default value for the value type.

For example:

mapping(address => bool) public approved;
mapping(address => uint256) public balances;

If a key has never been written:

  • approved[someAddress] returns false
  • balances[someAddress] returns 0

This is convenient, but it can also hide bugs if you assume a value was explicitly set. In practice, you often need a separate existence flag when 0 is a meaningful value.

Example: distinguishing “unset” from “set to zero”

struct Account {
    uint256 balance;
    bool exists;
}

mapping(address => Account) private accounts;

Now you can check:

if (!accounts[user].exists) {
    // user has never been initialized
}

This pattern is useful when zero is a valid stored value and you need to know whether a record was initialized.

Mappings cannot be iterated directly

A mapping does not store a list of keys, so Solidity cannot loop over all entries in a mapping by itself. This is a deliberate design choice that keeps storage efficient.

What this means in practice

You can do this:

uint256 amount = balances[user];

But you cannot do this:

for (uint256 i = 0; i < balances.length; i++) {
    // not possible
}

Because mappings are not enumerable, you should not use them when your contract must frequently list all entries on-chain.

Common workaround: store keys separately

If you need iteration, keep an auxiliary array of keys:

address[] private depositors;
mapping(address => bool) private seen;
mapping(address => uint256) public deposits;

function deposit() external payable {
    if (!seen[msg.sender]) {
        seen[msg.sender] = true;
        depositors.push(msg.sender);
    }
    deposits[msg.sender] += msg.value;
}

This lets you iterate over depositors while still using the mapping for fast lookups.

Trade-off summary

ApproachStrengthWeakness
Mapping onlyFast lookup, simple storageNo direct iteration
Array onlyEasy to enumerateSlow lookup by key
Mapping + arrayFast lookup and iterationMore storage and bookkeeping

Use the combined pattern only when enumeration is truly necessary.

Nested mappings for structured state

Mappings can map to other mappings, which is useful for multi-dimensional state.

mapping(address => mapping(address => uint256)) public allowances;

This is the basis of ERC-20 allowance tracking: one address grants another address permission to spend a certain amount.

Example: per-user permissions

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Permissions {
    mapping(address => mapping(bytes32 => bool)) private permissions;

    function grant(address user, bytes32 role) external {
        permissions[user][role] = true;
    }

    function revoke(address user, bytes32 role) external {
        permissions[user][role] = false;
    }

    function hasRole(address user, bytes32 role) external view returns (bool) {
        return permissions[user][role];
    }
}

Nested mappings are ideal when the first key identifies an account and the second key identifies a resource, role, or asset.

Mappings with structs

Mappings often store structs to keep related data together.

struct Profile {
    string name;
    uint256 points;
    bool active;
}

mapping(address => Profile) private profiles;

This is cleaner than maintaining several separate mappings for the same entity.

Example: updating a struct in a mapping

function setProfile(string calldata name) external {
    profiles[msg.sender] = Profile({
        name: name,
        points: 0,
        active: true
    });
}

You can also update fields individually:

profiles[msg.sender].points += 10;
profiles[msg.sender].active = false;

Best practice

When using structs in mappings, keep the struct focused on one conceptual entity. If the struct becomes too large or mixes unrelated concerns, the contract becomes harder to maintain.

Storage layout and gas considerations

Mappings live in contract storage, which is persistent and expensive to write. Reads are cheaper than writes, but both should be designed carefully.

Practical gas tips

  • Prefer compact value types when possible, such as uint128 or bool, if they fit your domain.
  • Avoid unnecessary writes to storage.
  • Update only the fields you need in a struct.
  • Use calldata for external function parameters when you only need to read them.
  • Keep mapping keys stable and deterministic.

Example: avoid redundant writes

function setActive(address user, bool value) external {
    if (profiles[user].active != value) {
        profiles[user].active = value;
    }
}

This prevents an unnecessary storage write when the value is already correct.

Choosing the right key type

Solidity supports several key types in mappings, but not all types are allowed.

Common key types

  • address
  • uint256 and other unsigned integers
  • bytes32
  • bool
  • enum

Usually avoid as keys

  • dynamic arrays
  • string
  • complex structs

If you need to use a string-like identifier, convert it to bytes32 off-chain or hash it with keccak256 before storing it.

mapping(bytes32 => uint256) public scores;

function setScore(string calldata name, uint256 score) external {
    bytes32 key = keccak256(bytes(name));
    scores[key] = score;
}

This is common when you want a compact identifier for labels, usernames, or slugs.

Common mistakes to avoid

1. Assuming a missing key means an error

A missing mapping entry returns a default value, not a revert. Always validate state explicitly when absence matters.

2. Trying to enumerate a mapping directly

Mappings are not iterable. If you need enumeration, track keys separately.

3. Using string keys without a plan

Strings are not valid mapping keys in the straightforward way many beginners expect. Prefer bytes32 or hashed identifiers.

4. Forgetting initialization logic

If a struct in a mapping needs an existence check, include a flag such as exists or initialized.

5. Overusing mappings for data that should be indexed differently

If your contract needs frequent ordered traversal, a mapping may not be the best primary structure. Consider arrays, linked structures, or off-chain indexing.

A practical example: simple whitelist

The following contract demonstrates a realistic whitelist pattern using a mapping:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Whitelist {
    address public owner;
    mapping(address => bool) public allowed;

    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }

    constructor() {
        owner = msg.sender;
    }

    function add(address user) external onlyOwner {
        allowed[user] = true;
    }

    function remove(address user) external onlyOwner {
        allowed[user] = false;
    }

    function isAllowed(address user) external view returns (bool) {
        return allowed[user];
    }
}

Why this design works

  • Lookup is constant time.
  • The public getter makes verification easy.
  • The mapping defaults to false, which matches the “not allowed” state.
  • The contract remains simple and readable.

This pattern is widely used for allowlists, admin sets, and feature flags.

When not to use a mapping

Mappings are not always the right answer. Avoid them when:

  • you need to list all entries on-chain
  • ordering matters
  • the dataset is small and fixed
  • the key space is not naturally hashable
  • you need compact historical snapshots of all values

For example, if you are storing a short list of predefined options, an array or enum may be simpler. If you need analytics over all records, emit events and index them off-chain.

Summary

Mappings are one of the most important Solidity storage tools because they provide fast, direct access to contract state by key. They are ideal for balances, permissions, settings, and other lookup-heavy data. The main limitations are that they cannot be iterated directly and they return default values for missing keys, so you must design around those behaviors.

A good rule of thumb is simple: use mappings when you need efficient keyed access, and add auxiliary structures only when enumeration or existence tracking is truly required.

Learn more with useful resources