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;
}