Why does storage layout dominate the gas cost of a contract?
Solidity storage layout dominates gas cost because persistent 32-byte slots are expensive to read and write on every node. Efficient smart contracts pack fields, cache repeated storage reads, avoid unnecessary writes and preserve layout during upgrades.
What the interviewer is scoring
- Whether you explain the pricing by what it is charging for, rather than reciting a list of tricks
- Does the candidate distinguish storage from memory and calldata when reasoning about cost
- That they know slot packing depends on declaration order and only helps adjacent fields
- Whether the answer reaches for removing writes entirely before reaching for smaller integer types
- Whether they can say when a micro-optimisation makes the contract more expensive
Answer
Short answer
Storage is expensive because every persistent slot must be maintained by the network. Optimize gas by reducing SSTORE operations, packing smaller values into the same slot when it is natural, caching repeated reads in memory, and never reordering stored fields in upgradeable contracts.
What gas is charging you for
Gas prices resource consumption, and the resources are not priced alike. Arithmetic is cheap because it happens once and leaves nothing behind. Memory is cheap because it lasts for the duration of a call. Contract storage is expensive because every node that validates the chain, now and in the future, has to carry it — a write is a permanent addition to state that thousands of machines will hold and re-serve indefinitely.
That asymmetry is the whole basis of gas optimisation. A loop doing hundreds of arithmetic operations is usually irrelevant next to one avoidable storage write in the same function. Candidates who arrive with a list of tricks and no ranking tend to spend their effort on the cheap end, which is how a "gas-optimised" rewrite ends up saving nothing measurable.
Two further distinctions matter. Storage is addressed in slots of 32 bytes, and the slot, not the variable, is the unit of cost. And the first access to a slot within a transaction is charged more than subsequent accesses to the same slot, because the first one has to be fetched from state while later ones are already in the machine's view — the cold and warm distinction, which is why touching the same slot twice is much cheaper than touching two slots once each.
Packing depends on declaration order
Solidity lays out state variables in declaration order and puts a variable in the current slot if it fits in the bytes remaining. So two uint128 fields declared next to each other share one slot, and the same two separated by a uint256 occupy two.
// Three slots. The uint256 cannot share, so each uint128 gets its own slot.
struct BadOrder {
uint128 price;
uint256 total;
uint128 filled;
}
// Two slots. price and filled are adjacent, so they pack into one.
struct GoodOrder {
uint128 price;
uint128 filled;
uint256 total;
}
The saving is real and it is structural: an order written once per trade costs a slot fewer for every trade the contract ever processes, and the change is a reordering with no behavioural consequence. This is the one optimisation that is nearly free to apply, which is why it is the first thing an interviewer expects you to reach for when a struct is on the whiteboard.
Packing has a boundary worth stating. It only applies to state variables and struct fields laid out in storage. Local variables, memory structs and function parameters are word-aligned regardless, so declaring a memory counter as uint8 saves nothing and can cost a little, because the compiler inserts masking to keep the narrow value in range.
Reads in a loop, and where the real savings are
A storage read inside a loop condition is charged on every iteration, including the array length.
// holders.length is a storage read, re-charged each time the condition runs.
for (uint256 i = 0; i < holders.length; i++) {
total += holders[i].amount;
}
// Read the length once into a local, which lives in memory.
uint256 n = holders.length;
for (uint256 i = 0; i < n; i++) {
total += holders[i].amount;
}
The same pattern applies to any storage value read more than once in a function: copy it into a local, work on the local, write back once at the end. That collapses several charged writes into one and turns repeated first-time reads into a single one.
The larger wins come from not writing at all. Data that only needs to be observable rather than readable by the contract belongs in an event, because logs are not part of the state a contract can query and are priced accordingly — an off-chain indexer reconstructs them perfectly well. Configuration that never changes belongs in constant or immutable, which the compiler places in the contract's code rather than in a storage slot. An allowlist of ten thousand addresses does not need ten thousand storage slots; it needs one slot holding a merkle root, with the caller supplying a proof. Each of these removes an entire class of write instead of shaving a slot off one.
Function arguments are the other place to look. Marking a large array parameter calldata rather than memory in an external function avoids copying it, because calldata is already there to be read. That costs nothing to change and matters most exactly where the input is biggest.
Where an optimisation turns into a regression
The failure mode here is optimising the declaration and pessimising the access pattern. If you pack four uint64 counters into one slot and then write them in four separate transactions, each write must read the slot, mask in the new value and write it back — you have added shifting and masking to every one of them while sharing a slot that has to be re-fetched each time anyway. Packing pays when the packed fields are written together, in the same transaction, which is why it fits a struct that is created and updated as a unit and fits a set of independently-updated globals badly.
The same judgement applies to loop unrolling, assembly rewrites of things the compiler already does well, and shortening revert strings. They are measurable, occasionally, and they cost you reviewability permanently. The order to work in is: remove the write, then reduce the number of slots, then reduce the number of accesses, and only then consider the byte-level tricks. Anything that makes the contract harder to audit has to be justified against the cost of a bug, and on a contract holding value that cost is not comparable to a gas saving.
State the reasoning as a hierarchy in the interview and measure before and after with the same tooling for both. An optimisation that nobody measured is a claim, and a claim about gas is exactly the sort of thing an interviewer will ask you to substantiate.
The unit of cost is the slot you touch and whether you have touched it before in this transaction, not the number of variables you declared.
© 2026 Preptima. Originally published at preptima.com.
Likely follow-ups
- What does the cold-versus-warm distinction for a storage slot mean within a single transaction?
- When is emitting an event a better choice than storing the same value?
- Why do constant and immutable variables not consume a storage slot?
- How does a merkle root change the cost profile of an allowlist compared with a mapping?
Related questions
- Walk me through a re-entrancy attack, and how checks-effects-interactions prevents itmediumAlso on solidity and evm4 min
- How does a proxy make a deployed contract upgradeable, and what does that put at risk?hardAlso on solidity5 min
- A modal passed design and QA review, but keyboard users report they can tab out of it into the page behind, and once they do they cannot get back or close it. Diagnose it and tell me what a correct dialog does.hardSame kind of round: concept4 min
- A Playwright test clicks a button that is visibly on screen, and roughly one run in twenty nothing happens. The button is server-rendered but the app hydrates after load. Why does the click get lost, and how do you fix the flake properly?mediumSame kind of round: coding4 min