Section 01 · Definition
What is a struct in Solidity?
A struct is a programmer-defined record type. You list the fields, give them names and types, and Solidity treats the result as a first-class type you can declare, pass around, and store.
Quick answer
What is a struct? A struct is a named bundle of fields that the compiler treats as a single type. Each field has its own type (uint, address, bool, mapping, even another struct) and Solidity lays the fields out in storage in declaration order, packing adjacent small fields into shared 32 byte slots when it can.
struct Account {
uint256 balance;
bool isActive;
uint64 lastSeen;
address referrer;
}
Account public alice;
function initAlice() external {
alice = Account({
balance: 100,
isActive: true,
lastSeen: uint64(block.timestamp),
referrer: msg.sender
});
}Section 02 · Storage packing
Field order changes how many slots you use
Two structs with the same fields in different orders can use very different amounts of storage. The savings are large enough to justify thinking about layout.
// Bad — 3 storage slots
struct UnpackedAccount {
uint256 balance; // slot 0 (32 bytes)
bool isActive; // slot 1 (1 byte, 31 wasted)
uint256 lastSeen; // slot 2 (32 bytes)
}
// Good — 2 storage slots, saves 20 000 gas per write
struct PackedAccount {
uint256 balance; // slot 0
bool isActive; // slot 1, byte 0
uint64 lastSeen; // slot 1, bytes 1-8
uint160 referrer; // slot 1, bytes 9-29
// 2 bytes left over — could pack one more uint16
}The rule is: any sequence of fields whose total size adds up to 32 bytes or less ends up in one slot. Pair a bool (1 byte) with a uint64 (8 bytes) and an address (20 bytes) — total 29 bytes — and the compiler packs them.
Section 03 · The pattern
mapping(address => struct) is the workhorse
Almost every contract that tracks per-user state uses this exact shape.
struct Position {
uint256 collateral;
uint256 debt;
uint64 openedAt;
}
mapping(address => Position) public positions;
function open(uint256 collateral, uint256 debt) external {
positions[msg.sender] = Position({
collateral: collateral,
debt: debt,
openedAt: uint64(block.timestamp)
});
}
function addCollateral(uint256 amount) external {
Position storage p = positions[msg.sender]; // reference, not copy
p.collateral += amount;
}Two important details. First, the assignment Position storage p = positions[msg.sender] creates a storage reference — modifying p modifies the underlying state. If you wrote Position memory p instead, you would copy the struct into memory and any changes would be silently discarded. Second, this pattern combines with mappings and arrays to model essentially every domain entity in DeFi.
Always specify storage or memory for struct locals
If you declare 'Position p = positions[user]' without specifying memory or storage, modern Solidity will refuse to compile. Older versions would silently default to storage and create surprising aliasing bugs. The explicit keyword keeps the intent obvious.
Section 04 · Real-world uses
Where structs show up in production code
// 1. Vesting schedule
struct Vesting {
uint256 totalAmount;
uint256 claimed;
uint64 start;
uint64 cliff;
uint64 duration;
bool revoked;
}
mapping(address => Vesting) public vestings;
// 2. ERC721 royalty information (EIP-2981 reference style)
struct RoyaltyInfo {
address receiver;
uint96 feeBasisPoints;
}
mapping(uint256 => RoyaltyInfo) public royaltyOf;
// 3. Governance proposal
struct Proposal {
uint256 id;
string title;
address proposer;
uint64 start;
uint64 end;
uint128 yesVotes;
uint128 noVotes;
bool executed;
}
Proposal[] public proposals;
// 4. EIP-712 typed data
struct Permit {
address owner;
address spender;
uint256 value;
uint256 nonce;
uint256 deadline;
}Section 06 · FAQ
Frequently asked questions
Does field order in a struct affect gas cost?
Yes — when the struct lives in storage. The compiler lays fields out in declaration order and packs adjacent small fields into the same 32 byte slot. Putting a uint256 between two bools wastes the packing opportunity. Group small fields together.
Can a struct contain another struct?
Yes. Nested structs are common — for example a Position struct that contains a CollateralAsset struct. The compiler flattens the layout so the nested struct's fields participate in the same packing logic as the parent's.
Can a struct contain a mapping?
Yes, but only when the struct itself lives in storage. Mappings cannot exist in memory or calldata, so any struct that contains a mapping cannot be passed around or returned by value.
What is the difference between assigning to a storage struct and a memory struct?
Storage assignment writes through to state — modifying a Position storage p reference modifies the contract's storage. Memory assignment makes a copy — modifying a Position memory p has no effect on storage. Specify the location explicitly to avoid the bug where you 'update' a struct that turns out to be a copy.
How do I return a struct from a function?
Declare the return type and return a struct literal: function getAlice() external view returns (Account memory) { return alice; }. The struct is copied into memory and ABI-encoded for the caller.