# Understanding Solidity Mappings — From Basics to Advanced Patterns
Mappings are one of the most fundamental data structures in Solidity, yet they're often misunderstood by developers coming from other languages. This guide will take you from basic mapping concepts to advanced patterns used in production smart contracts.
What Are Mappings?
A mapping is a hash table data structure that associates keys with values. Think of it as a dictionary or hash map in other programming languages, but with some unique blockchain-specific characteristics.
// Basic mapping declaration
mapping(address => uint256) public balances;
// Using the mapping
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function getBalance(address user) public view returns (uint256) {
return balances[user];
}
Key Characteristics
Mappings vs Arrays
Many developers wonder when to use mappings versus arrays. Here's a practical comparison:
contract MappingVsArray {
// Mapping: O(1) lookup, no iteration
mapping(address => bool) public whitelist;
// Array: Can iterate, but O(n) lookup
address[] public whitelistArray;
// Mapping approach - very gas efficient
function addToWhitelist(address user) public {
whitelist[user] = true;
// Cost: ~20,000 gas for first write, ~5,000 for updates
}
// Array approach - can iterate but expensive
function addToWhitelistArray(address user) public {
whitelistArray.push(user);
// Cost: ~44,000+ gas and grows with array size
}
// Checking membership
function isWhitelisted(address user) public view returns (bool) {
return whitelist[user]; // O(1) - constant gas
}
function isWhitelistedArray(address user) public view returns (bool) {
for (uint i = 0; i < whitelistArray.length; i++) {
if (whitelistArray[i] == user) return true;
}
return false; // O(n) - gas grows linearly
}
}
Use mappings when:
- You need fast lookups by key
- You don't need to iterate over all values
- You want predictable gas costs
Use arrays when:
- You need to iterate over all elements
- You need to know the count of elements
- Order matters
Nested Mappings
Nested mappings unlock powerful multi-dimensional data structures:
contract TokenAllowances {
// owner => spender => amount
mapping(address => mapping(address => uint256)) public allowances;
function approve(address spender, uint256 amount) public {
allowances[msg.sender][spender] = amount;
}
function transferFrom(
address owner,
address recipient,
uint256 amount
) public {
require(allowances[owner][msg.sender] >= amount, "Insufficient allowance");
allowances[owner][msg.sender] -= amount;
// ... transfer logic
}
}
Triple Nested Mappings
For complex use cases like multi-token DEXs:
contract MultiTokenExchange {
// user => token => exchange => balance
mapping(address => mapping(address => mapping(address => uint256))) public deposits;
function depositToExchange(
address token,
address exchange,
uint256 amount
) public {
deposits[msg.sender][token][exchange] += amount;
// ... transfer logic
}
}
The Iterable Mapping Pattern
The biggest limitation of mappings is that you cannot iterate over them. Here's a battle-tested pattern to make them iterable:
contract IterableMapping {
struct UserInfo {
uint256 balance;
uint256 index; // Position in the keys array
bool exists;
}
mapping(address => UserInfo) public users;
address[] public keys;
function set(address key, uint256 value) public {
if (!users[key].exists) {
// New user - add to keys array
users[key].exists = true;
users[key].index = keys.length;
keys.push(key);
}
users[key].balance = value;
}
function remove(address key) public {
require(users[key].exists, "User does not exist");
// Move the last element to the deleted spot
uint256 indexToDelete = users[key].index;
address lastKey = keys[keys.length - 1];
keys[indexToDelete] = lastKey;
users[lastKey].index = indexToDelete;
keys.pop();
delete users[key];
}
function getAll() public view returns (address[] memory) {
return keys;
}
function size() public view returns (uint256) {
return keys.length;
}
function iterate(uint256 start, uint256 count)
public
view
returns (address[] memory addresses, uint256[] memory balances)
{
require(start + count <= keys.length, "Out of bounds");
addresses = new address[](count);
balances = new uint256[](count);
for (uint256 i = 0; i < count; i++) {
address key = keys[start + i];
addresses[i] = key;
balances[i] = users[key].balance;
}
}
}
This pattern is used in protocols like Uniswap v2 for tracking all pairs and in staking contracts for reward distribution.
Gas Optimization Techniques
1. Packing Values in Structs
When using mappings with structs, pack values carefully:
// Bad - each field takes a full slot
struct User {
uint256 balance; // 32 bytes
uint256 lastUpdate; // 32 bytes
bool isActive; // 32 bytes (wasteful!)
}
// Good - packed into fewer slots
struct UserOptimized {
uint128 balance; // 16 bytes
uint64 lastUpdate; // 8 bytes
uint32 rewardRate; // 4 bytes
uint8 tier; // 1 byte
bool isActive; // 1 byte
// Total: 30 bytes = 1 storage slot
}
mapping(address => UserOptimized) public users;
2. Avoid Unnecessary Reads
// Bad - reads from storage 3 times
function processUser(address user) public {
if (users[user].balance > 0) {
users[user].balance -= 100;
users[user].lastUpdate = block.timestamp;
}
}
// Good - read once, write once
function processUserOptimized(address user) public {
UserOptimized memory u = users[user];
if (u.balance > 0) {
u.balance -= 100;
u.lastUpdate = uint64(block.timestamp);
users[user] = u;
}
}
3. Delete to Get Gas Refunds
function clearUser(address user) public {
delete users[user]; // Get gas refund for clearing storage
}
Advanced Pattern: Enumerable Set
Using OpenZeppelin's EnumerableSet with mappings:
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
contract RoleManager {
using EnumerableSet for EnumerableSet.AddressSet;
mapping(bytes32 => EnumerableSet.AddressSet) private roleMembers;
function grantRole(bytes32 role, address account) public {
roleMembers[role].add(account);
}
function revokeRole(bytes32 role, address account) public {
roleMembers[role].remove(account);
}
function getRoleMemberCount(bytes32 role) public view returns (uint256) {
return roleMembers[role].length();
}
function getRoleMember(bytes32 role, uint256 index) public view returns (address) {
return roleMembers[role].at(index);
}
}
Common Pitfalls
1. Assuming Deletion Resets Everything
struct User {
uint256 balance;
address[] friends;
}
mapping(address => User) public users;
// This does NOT delete the friends array!
delete users[msg.sender];
// You must manually delete nested dynamic arrays
delete users[msg.sender].friends;
delete users[msg.sender];
2. Using Mappings as Function Parameters
Mappings can only be used as storage references:
// This will NOT compile
function process(mapping(address => uint256) memory data) public {
// Error: mappings cannot be memory
}
// Instead, pass storage reference (internal/private only)
function process(mapping(address => uint256) storage data) internal {
// This works
}
Conclusion
Mappings are the backbone of efficient Solidity development. Master these patterns and you'll write cleaner, more gas-efficient smart contracts. Remember:
- Use mappings for O(1) lookups
- Implement the iterable pattern when you need enumeration
- Pack struct values to save gas
- Use EnumerableSet for advanced use cases
- Always consider gas costs when designing your data structures
Start experimenting with these patterns in your next project, and you'll quickly see why mappings are so powerful in Solidity development.