Solidity Basics (Part 2) — Arrays, Mappings & Structs (Upgrading the Web3 Journey Logger)

Yesterday we took our first real steps into Solidity: variables, functions, and a simple Web3JourneyLogger contract that stored a single note on-chain. Today we’re going to upgrade that tiny contract into something closer to how real dApps manage data.

This is Day 27 of the 60‑Day Web3 journey, still in Phase 3: Development. The goal for today is to understand how Solidity handles collections of data : arrays, mappings, and structs and then use them together to build a multi‑entry, multi‑user on-chain journal.

1. Why we need more than simple variables

Storing a single dayNumber and note is cute, but it doesn’t scale.

Real smart contracts usually need to:

  • Track many pieces of data, not just one (multiple entries, multiple users).
  • Group related data into records (like “user profile”, “order”, “position”).
  • Look up data quickly by a key like an address or an ID.

In normal programming, you’d reach for arrays, dictionaries, and objects. In Solidity, you get very similar tools:

  • Arrays → ordered lists of items.
  • Mappings → key → value lookups (like hash maps).
  • Structs → custom data types that bundle fields together.

We’ll see each one separately, then combine them into a better Web3 Journey Logger.

2. Arrays: storing ordered lists

An array in Solidity is an ordered list of elements of the same type.

2.1 Fixed vs dynamic arrays

There are two main kinds of arrays:

  • Fixed-size array
  uint256 public fixedNumbers;

This always has exactly 5 elements. You cannot grow or shrink it.

  • Dynamic array
  uint256[] public numbers;

This can grow as you push new elements to it.

For most dApp use cases, you’ll use dynamic arrays.

2.2 Basic operations on dynamic arrays

Here’s a tiny example:

uint256[] public daysLearned;

function addDay(uint256 _day) public {
    daysLearned.push(_day); // add element at the end
}

function getDayAtIndex(uint256 index) public view returns (uint256) {
    return daysLearned[index];
}

function getTotalDays() public view returns (uint256) {
    return daysLearned.length;
}

Key points:

  • push appends a new element.
  • You access elements with array[index].
  • You can check array.length to know how many items are stored.

Arrays are great for ordered lists, but they aren’t efficient for lookups like “give me the entry for this address”. For that, we use mappings.

3. Mappings: key–value storage on-chain

A mapping is like a dictionary or hash map: you give it a key, and it gives you a value.

3.1 Basic mapping syntax

The general form:

mapping(KeyType => ValueType) public myMapping;

Example:

mapping(address => uint256) public entryCountByUser;

This mapping says: “for each address, store a uint256 count”.

If you do:

entryCountByUser[msg.sender] = 5;

then entryCountByUser[msg.sender] will later return 5.

3.2 Important mapping quirks

Mappings have some important properties:

  • They behave like infinite default dictionaries.
    • Any key you haven’t set yet returns the default value for that type (0, false, address 0x0, etc.).
  • They are not iterable.

    • You can’t “loop over all keys” from inside the contract.
    • If you need iteration, you must keep a separate array of keys or a counter.

Because of these quirks, mappings are best used for “given a key, fetch the value” patterns, like:

  • address → user profile
  • token ID → owner
  • order ID → order struct

4. Structs: custom data types

A struct lets you define your own data shape by grouping fields together.

4.1 Defining and using a struct

Example struct:

struct Entry {
    uint256 day;
    string note;
}

You can use it like this:

Entry public latestEntry;

function setLatestEntry(uint256 _day, string calldata _note) public {
    latestEntry = Entry({day: _day, note: _note});
}

Structs are especially powerful when combined with arrays and mappings.

5. Upgrading the Web3 Journey Logger

Let’s upgrade yesterday’s contract to support multiple entries per user and multiple users.

5.1 New design

We want:

  • A struct Entry representing (day, note).
  • For each user address, an array of Entry structs.
  • Helper functions to:
    • Add a new entry.
    • Read a single entry by index.
    • Get how many entries a user has.

This leads to a pattern like:

mapping(address => Entry[]) public entriesByUser;

Which you can read as: “for each address, store an array of Entry structs”.

5.2 Full upgraded contract

Here’s a full version of an upgraded Web3JourneyLoggerV2:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Web3JourneyLoggerV2 {
    // --- Structs ---

    struct Entry {
        uint256 day;
        string note;
    }

    // --- State variables ---

    // Address of the person who deployed the contract
    address public owner;

    // Name or handle of the learner (optional global name)
    string public name;

    // For each user address, store an array of their entries
    mapping(address => Entry[]) private entriesByUser;

    // --- Events ---

    event EntryAdded(address indexed user, uint256 indexed day, uint256 index, string note);

    // --- Constructor ---

    constructor(string memory _name) {
        owner = msg.sender;
        name = _name;
    }

    // --- Core functions ---

    /// @notice Add a new journal entry for the caller
    function addEntry(uint256 _day, string calldata _note) public {
        Entry memory newEntry = Entry({day: _day, note: _note});
        entriesByUser[msg.sender].push(newEntry);

        uint256 index = entriesByUser[msg.sender].length - 1;
        emit EntryAdded(msg.sender, _day, index, _note);
    }

    /// @notice Get a specific entry for a user by index
    function getEntry(address _user, uint256 _index)
        public
        view
        returns (uint256 day, string memory note)
    {
        require(_index < entriesByUser[_user].length, "Index out of bounds");
        Entry storage entry = entriesByUser[_user][_index];
        return (entry.day, entry.note);
    }

    /// @notice Get how many entries a user has
    function getEntryCount(address _user) public view returns (uint256) {
        return entriesByUser[_user].length;
    }
}

A few things to notice:

  • Entry is a struct containing day and note.
  • mapping(address => Entry[]) private entriesByUser; creates a mapping from user to an array of their entries.
  • addEntry builds a new Entry in memory, pushes it into the caller’s array, and emits an EntryAdded event.
  • getEntry lets you read a specific entry by user and index.
  • getEntryCount tells you how many entries a user has so front-ends can loop over them off-chain.

This contract is no longer just a single note; it’s a tiny, multi‑user journaling dApp backend.

6. Deploying V2 and testing it

You can deploy Web3JourneyLoggerV2 using the same Remix + Sepolia flow from yesterday:

  1. Open Remix and create Web3JourneyLoggerV2.sol.
  2. Paste the full contract above.
  3. Compile with a 0.8.x compiler.
  4. In “Deploy & Run Transactions”, choose Injected Provider – MetaMask and the Sepolia network.
  5. Deploy with a name (e.g., "Web3ForHumans").

Once deployed:

  • Call addEntry(27, "Learned arrays, mappings, and structs today") from your wallet.
  • Call getEntryCount(yourAddress) — you should see 1.
  • Call getEntry(yourAddress, 0) to read back the first entry.

Ask a friend to connect their wallet and call addEntry too. Now your contract is tracking multiple learners and their progress on-chain.

7. Why arrays + mappings + structs matter

These three tools — arrays, mappings, and structs — are the backbone of almost every serious Solidity contract:

  • A DEX might use mapping(address => mapping(address => uint256)) to track token balances.
  • A DAO might use structs + arrays for proposals and votes.
  • An NFT contract uses mappings from token IDs to owners and metadata.

By understanding how to combine them, you’re no longer just “deploying example contracts” — you’re modeling real-world data structures on-chain.

Tomorrow, we can build on this by adding more features like:

  • Editing or deleting entries.
  • Restricting certain actions to the contract owner.
  • Or exposing your journal data to a simple frontend.

For now, if you deploy Web3JourneyLoggerV2 on Sepolia, share your contract address — let’s see how many on-chain learning journals we can spin up.

Further reading

Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post

Why Traditional DevOps Stops Scaling

Next Post

Kubernetes Tutorial for Beginners: Basic to Advance

Related Posts