
Solidity Storage Layout and Packing: Designing Efficient State for Gas and Upgrade Safety
Why storage layout matters
Solidity stores contract state in 32-byte slots. Simple values may share a slot if they fit, while larger values such as mappings and dynamic arrays use more complex storage rules. The compiler determines layout at compile time, and that layout becomes part of your contract’s long-term behavior.
You should care about storage layout for three main reasons:
- Gas efficiency: fewer storage slots often means fewer expensive
SSTOREandSLOADoperations. - Upgrade safety: proxy-based systems depend on the implementation contract preserving the same storage layout across versions.
- Correctness: changing variable order or type can silently break assumptions about where data lives.
A contract with poor storage design may still compile and deploy successfully, but it can become expensive to use or dangerous to upgrade.
How Solidity assigns storage slots
State variables are assigned storage slots in declaration order, starting at slot 0. Solidity tries to pack smaller types into a single 32-byte slot when possible.
Packing rules in practice
Variables can share a slot if:
- they are declared consecutively,
- their combined size fits within 32 bytes,
- and they are value types such as
uint,int,bool,address, or fixed-size bytes.
If a variable does not fit in the remaining space, Solidity starts a new slot.
Example of packing
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract PackedExample {
uint128 public a; // slot 0, lower 16 bytes
uint64 public b; // slot 0, next 8 bytes
bool public c; // slot 0, next 1 byte
address public d; // slot 1, because it does not fit in remaining space
}In this example, a, b, and c can share slot 0, while d occupies slot 1. This reduces storage usage compared to placing each variable in its own slot.
What does not pack
The following types do not behave like simple packed values:
mapping- dynamic arrays like
uint256[] bytesstring- structs containing dynamic members
These types reserve a slot for their “anchor,” but their actual data is stored elsewhere.
Choosing types for efficient storage
A common optimization is to choose the smallest type that still fits your domain. However, smaller is not always better. You should balance packing with readability, arithmetic safety, and future growth.
| Type choice | Benefit | Tradeoff |
|---|---|---|
uint256 | Native word size, simplest arithmetic | May waste space when packing is possible |
uint128 / uint64 | Better packing density | Requires careful range planning |
bool | Compact flag storage | Multiple booleans can still be awkward to update |
address | Clear semantic meaning | Uses 20 bytes, may limit packing combinations |
bytes32 | Fixed-size and hash-friendly | Less expressive than structured types |
Practical guidance
Use smaller types when:
- the value has a known upper bound,
- you store many instances of the same field,
- and the field is frequently read or written alongside other packed values.
Prefer uint256 when:
- the value is used in arithmetic-heavy logic,
- the range is expected to grow,
- or the variable is rarely stored and packing does not matter.
For example, balances, timestamps, and counters are often stored as uint256 for simplicity. But configuration flags, role IDs, or small enumerations can often be packed into smaller types.
Structs, arrays, and mappings
Composite types require special attention because their storage behavior differs from simple values.
Structs
Struct members are laid out in order and can be packed just like top-level variables.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract StructPacking {
struct Position {
uint128 amount;
uint64 openedAt;
bool active;
}
Position public position;
}This struct is compact because its fields can share storage efficiently. If you add a dynamic field like string, the layout becomes more complex and less predictable.
Arrays
Fixed-size arrays are stored inline if their elements are value types. Dynamic arrays store their length in one slot, and their elements begin at a derived storage location.
Mappings
Mappings are not iterable by default and do not store their contents contiguously. The mapping slot acts as a seed for computing the storage location of each key-value pair.
This means mappings are excellent for lookup-heavy data, but they are not suitable when you need ordered traversal without additional indexing structures.
Storage packing tradeoffs
Packing is not always a free win. Reading or writing packed values may require extra bit masking and shifting under the hood. In many cases, the gas savings from reduced slot usage outweigh the overhead, but not always.
When packing helps
Packing is usually beneficial when:
- several fields are updated together,
- the contract stores many records,
- or the data is mostly read rather than frequently rewritten.
When packing can hurt
Packing may be less useful when:
- a field is updated independently and often,
- the packed slot must be read-modified-written repeatedly,
- or the code becomes harder to maintain.
For example, if you frequently update a single bool that shares a slot with unrelated values, every write may require loading and rewriting the whole slot. In a hot path, that can reduce the benefit of packing.
A good rule is to group fields by access pattern, not just by size.
Storage layout and upgradeable contracts
Storage layout becomes critical in proxy-based upgradeable systems. The proxy holds state, while the implementation contract provides logic. If the new implementation changes the storage layout, existing data may be interpreted incorrectly.
Safe upgrade principles
Follow these rules:
- Never reorder existing state variables.
- Never change the type of an existing variable.
- Only append new variables at the end.
- Be careful when inserting variables into packed slots.
- Preserve inherited storage order across base contracts.
Example of a dangerous change
Suppose version 1 contains:
contract V1 {
uint256 public totalSupply;
address public owner;
}If version 2 changes the order:
contract V2 {
address public owner;
uint256 public totalSupply;
}The proxy will still read the old storage slots, but the new code will interpret them differently. That can corrupt the meaning of the stored values.
Best practice for upgrades
Treat storage layout as an external interface. Once deployed, it should be considered immutable unless you are using a deliberate migration strategy.
Inheritance and storage order
Inheritance affects storage layout because Solidity lays out state variables from base contracts before derived contracts. This matters in complex systems with multiple inheritance layers.
Example
contract BaseA {
uint256 internal a;
}
contract BaseB {
uint128 internal b;
}
contract Child is BaseA, BaseB {
bool internal c;
}The final layout depends on the inheritance order and the declaration order inside each contract. If you later reorder base contracts, you may change the storage layout even if the child contract’s own variables remain unchanged.
Best practice
When designing upgradeable or extensible systems:
- keep storage declarations centralized when possible,
- avoid changing inheritance order after deployment,
- and document the intended layout for future maintainers.
Reading and inspecting storage layout
Solidity compilers can emit storage layout metadata, which is useful for audits and upgrade reviews. This is especially important in CI pipelines for proxy contracts.
You can inspect layout using compiler output or tooling such as Hardhat, Foundry, or the Solidity compiler’s JSON output. The goal is to verify that new versions preserve slot positions and offsets.
What to look for
When reviewing layout, check:
- slot number,
- byte offset within the slot,
- type size,
- and whether a variable is packed with others.
If a new version introduces a variable in the middle of an existing layout, that is usually a red flag.
Practical patterns for efficient storage design
1. Group related small fields
If several fields are always used together, place them near each other so they can pack into the same slot.
struct Config {
uint64 feeBps;
uint64 maxUsers;
bool paused;
bool whitelistEnabled;
}2. Separate hot and cold data
Frequently updated values should not be forced to share slots with unrelated fields if that creates unnecessary write amplification.
3. Use mappings for sparse data
If most keys are empty, a mapping is often more efficient than a large array or nested struct tree.
4. Reserve upgrade space intentionally
For upgradeable contracts, leave room at the end of storage for future variables, or use a storage gap pattern if your architecture requires it.
5. Document slot assumptions
If off-chain systems, auditors, or other contracts depend on a specific layout, document it explicitly. Storage assumptions are part of your contract’s operational contract with the rest of the system.
Common mistakes to avoid
Changing variable order after deployment
This is the most common and most dangerous mistake in upgradeable systems.
Over-optimizing with tiny types
Using uint8 everywhere can make code harder to read and may not produce meaningful savings if the values are not packed well.
Packing unrelated variables
Packing should reflect access patterns and upgrade boundaries, not just storage density.
Ignoring inherited storage
Base contracts contribute to the final layout. A harmless-looking change in a parent contract can break the child.
Assuming mappings are iterable
Mappings are great for lookup, but if you need enumeration, maintain a separate array or index structure.
A practical checklist
Before finalizing a contract’s storage design, ask:
- Are the most frequently used values grouped sensibly?
- Do any variables need to remain stable for upgrades?
- Are small types chosen for a real reason, not just optimization theater?
- Could a packed slot create unnecessary read-modify-write overhead?
- Have I reviewed inheritance effects on layout?
- Is the layout documented and tested?
If the answer to any of these is unclear, revisit the design before deployment.
Conclusion
Storage layout is not just an implementation detail in Solidity. It directly affects gas usage, maintainability, and the safety of future upgrades. By understanding packing rules, inheritance order, and the behavior of composite types, you can design contracts that are both efficient and resilient.
The best storage design is usually the one that balances clarity with deliberate optimization. Pack where it helps, keep upgrade boundaries stable, and treat layout changes with the same caution as changes to public APIs.
