BlockchainSolidity8 min readUpdated

Structs in Solidity: Bundle Fields into a Domain Type

By Mudassir Khan — Agentic AI Consultant & AI Systems Architect, Islamabad, Pakistan

Cover illustration for: Structs in Solidity: Bundle Fields into a Domain Type

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.

solidity
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.

Two layouts of the same struct showing wasted slots when fields are unpacked and savings when they are packed.
Group small fields together so the compiler can fit them into one 32 byte slot.
solidity
// 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.

solidity
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

solidity
// 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.

Written by Mudassir Khan

Agentic AI consultant and AI systems architect based in Islamabad, Pakistan. CEO of Cube A Cloud. 38+ agentic AI launches delivered for global founders and CTOs.

View agentic AI consulting serviceSee ChainTrust case study

Related service

Agentic AI Consulting

See scope & pricing →

Related case study

ChainTrust Compliance Engine

Read case study →

More on this topic

Need an AI systems architect?

Book a 30-minute architecture call. I will sketch the high-level design for your use case and give you an honest view of the trade-offs.

Book a strategy call →