InterviewKata
Architecture styles · 13 of 16 · Scale and decentralisation

Space-based

Space-based architecture takes the database off the request path so that a sudden surge can't overwhelm it: copies of the app serve everything from replicated memory, and the database catches up in batches. This page builds Pedal's festival surge that way, survives a unit crash, and measures what the write-behind window puts at risk.

1. In one picture

At a stadium on match day, ticket checkers at every gate share one live list of who has entered, passed between them by radio. Nobody phones the head office for each fan; the office gets the full list after the match. More gates can open for the rush, and if one checker leaves, the others still have the list.

Space-based architecture removes the database from the request path. Each processing unit is a copy of the application that keeps the working data in memory, in a data grid replicated between the units. Writes go to memory (and to the other units), and a data writer saves them to the database later, in batches ("write-behind"). A data reader loads the grid from the database when the system starts.

It's built for sudden, extreme load on the same hot data: ticket on-sales, auctions, flash sales, a city-wide surge of rides. Because units share nothing but the grid, more can start in seconds. The name comes from tuple spaces, the shared-memory model it grew from; Architecture module 15 has the architect's view.

2. Pedal built this way

A festival ends and 20,000 people want bikes in the same half hour. Follow two unlocks, a database flush, and a unit crashing mid-surge.

3. The rules

RuleWhy it existsBroken when…
Requests are served from memoryThe database can't become the bottleneck under a surge.A processing unit reads the database for each unlock "to be safe".
The grid is replicated between unitsAny unit can serve any request; a unit's crash loses no data.Each unit keeps its own private copy, and riders see different bike counts.
The database is written later, in batchesWrites reach the database at a rate it can take.A synchronous database write on every request.
Units are disposableScaling up and down is just starting and stopping copies.A unit holds data no other unit has.

4. How it goes wrong

Data that doesn't fit in memoryThe grid holds the hot working set (open rides, today's bikes), not five years of history.
Ignoring the write-behind windowWrites not yet flushed exist only in memory; if every copy is lost, they're gone.
Conflicting writesTwo units updating the same bike at once need a rule (one owner per key, or versioned writes).
Using it for steady loadAll this machinery for traffic one database could handle is cost without benefit.

5. The code

Processing units that keep rides in a replicated in-memory grid, a write-behind queue, and a data writer that saves it to the database in one batch. When unit-2 crashes, unit-3 starts by copying the grid from unit-1.

from __future__ import annotations


class Database:                                   # slow, durable, updated later
    def __init__(self) -> None:
        self.rides: dict[str, dict] = {}


class ProcessingUnit:
    """A copy of the application holding its working data in memory, kept in step with its peers."""

    def __init__(self, name: str, peers: list["ProcessingUnit"], write_behind: list[tuple[str, dict]]) -> None:
        self.name, self.peers, self.queue = name, peers, write_behind
        self.grid: dict[str, dict] = {}
        for p in peers:                           # a new unit copies the grid from a running peer
            self.grid = {k: dict(v) for k, v in p.grid.items()}
            break
        peers.append(self)

    def put(self, key: str, value: dict) -> None:
        for p in self.peers:                      # replicate to every unit's memory
            p.grid[key] = dict(value)
        self.queue.append((key, dict(value)))     # the database hears about it later

    def unlock(self, rider: str, bike: str) -> str:
        ride_id = f"R{len(self.grid) + 1}"
        self.put(ride_id, {"rider": rider, "bike": bike, "open": True})
        return ride_id


def flush(queue: list[tuple[str, dict]], db: Database) -> int:   # the data writer, run every few seconds
    n = len(queue)
    for key, value in queue:
        db.rides[key] = value
    queue.clear()
    return n


if __name__ == "__main__":
    db, units, queue = Database(), [], []
    u1, u2 = ProcessingUnit("unit-1", units, queue), ProcessingUnit("unit-2", units, queue)
    print("unit-1 unlocked", u1.unlock("Mia", "B7"), "- grid updated, database write queued")
    print("unit-2 unlocked", u2.unlock("Leo", "B9"))
    print("unit-2 already sees R1:", u2.grid["R1"]["rider"], "| rides in the database:", len(db.rides))
    print("data writer flushed", flush(queue, db), "writes | rides in the database:", len(db.rides))
    units.remove(u2)                              # unit-2 crashes during the surge
    u3 = ProcessingUnit("unit-3", units, queue)
    print("unit-3 started and copied the grid:", sorted(u3.grid), "| units running:", [u.name for u in units])
interface Ride { rider: string; bike: string; open: boolean; }
class Database { rides = new Map<string, Ride>(); } // slow, durable, updated later

// A copy of the application holding its working data in memory, kept in step with its peers.
class ProcessingUnit {
  grid = new Map<string, Ride>();
  constructor(readonly name: string, private peers: ProcessingUnit[], private queue: [string, Ride][]) {
    if (peers.length) for (const [k, v] of peers[0].grid) this.grid.set(k, { ...v }); // a new unit copies the grid from a running peer
    peers.push(this);
  }
  put(key: string, value: Ride): void {
    for (const p of this.peers) p.grid.set(key, { ...value }); // replicate to every unit's memory
    this.queue.push([key, { ...value }]);                     // the database hears about it later
  }
  unlock(rider: string, bike: string): string {
    const id = `R${this.grid.size + 1}`;
    this.put(id, { rider, bike, open: true });
    return id;
  }
}

function flush(queue: [string, Ride][], db: Database): number { // the data writer, run every few seconds
  const n = queue.length;
  for (const [k, v] of queue) db.rides.set(k, v);
  queue.length = 0;
  return n;
}

const db = new Database(), units: ProcessingUnit[] = [], queue: [string, Ride][] = [];
const u1 = new ProcessingUnit("unit-1", units, queue), u2 = new ProcessingUnit("unit-2", units, queue);
console.log(`unit-1 unlocked ${u1.unlock("Mia", "B7")} - grid updated, database write queued`);
console.log(`unit-2 unlocked ${u2.unlock("Leo", "B9")}`);
console.log(`unit-2 already sees R1: ${u2.grid.get("R1")?.rider} | rides in the database: ${db.rides.size}`);
console.log(`data writer flushed ${flush(queue, db)} writes | rides in the database: ${db.rides.size}`);
units.splice(units.indexOf(u2), 1); // unit-2 crashes during the surge
const u3 = new ProcessingUnit("unit-3", units, queue);
console.log(`unit-3 started and copied the grid: [${[...u3.grid.keys()].sort().map((k) => `'${k}'`).join(", ")}] | units running: [${units.map((u) => `'${u.name}'`).join(", ")}]`);
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.stream.Collectors;

record Ride(String rider, String bike, boolean open) {}

class Database { final Map<String, Ride> rides = new LinkedHashMap<>(); } // slow, durable, updated later

// A copy of the application holding its working data in memory, kept in step with its peers.
class ProcessingUnit {
    final String name; final Map<String, Ride> grid = new LinkedHashMap<>();
    private final List<ProcessingUnit> peers; private final List<Map.Entry<String, Ride>> queue;
    ProcessingUnit(String name, List<ProcessingUnit> peers, List<Map.Entry<String, Ride>> queue) {
        this.name = name; this.peers = peers; this.queue = queue;
        if (!peers.isEmpty()) grid.putAll(peers.get(0).grid); // a new unit copies the grid from a running peer
        peers.add(this);
    }
    void put(String key, Ride value) {
        for (ProcessingUnit p : peers) p.grid.put(key, value); // replicate to every unit's memory
        queue.add(Map.entry(key, value));                     // the database hears about it later
    }
    String unlock(String rider, String bike) {
        String id = "R" + (grid.size() + 1);
        put(id, new Ride(rider, bike, true));
        return id;
    }
}

public class Main {
    static int flush(List<Map.Entry<String, Ride>> queue, Database db) { // the data writer, run every few seconds
        int n = queue.size();
        for (Map.Entry<String, Ride> e : queue) db.rides.put(e.getKey(), e.getValue());
        queue.clear();
        return n;
    }
    public static void main(String[] args) {
        Database db = new Database(); List<ProcessingUnit> units = new ArrayList<>(); List<Map.Entry<String, Ride>> queue = new ArrayList<>();
        ProcessingUnit u1 = new ProcessingUnit("unit-1", units, queue), u2 = new ProcessingUnit("unit-2", units, queue);
        System.out.println("unit-1 unlocked " + u1.unlock("Mia", "B7") + " - grid updated, database write queued");
        System.out.println("unit-2 unlocked " + u2.unlock("Leo", "B9"));
        System.out.println("unit-2 already sees R1: " + u2.grid.get("R1").rider() + " | rides in the database: " + db.rides.size());
        System.out.println("data writer flushed " + flush(queue, db) + " writes | rides in the database: " + db.rides.size());
        units.remove(u2); // unit-2 crashes during the surge
        ProcessingUnit u3 = new ProcessingUnit("unit-3", units, queue);
        System.out.println("unit-3 started and copied the grid: [" + new TreeSet<>(u3.grid.keySet()).stream().map(k -> "'" + k + "'").collect(Collectors.joining(", "))
            + "] | units running: [" + units.stream().map(u -> "'" + u.name + "'").collect(Collectors.joining(", ")) + "]");
    }
}

Output (the same in all three languages)

unit-1 unlocked R1 - grid updated, database write queued
unit-2 unlocked R2
unit-2 already sees R1: Mia | rides in the database: 0
data writer flushed 2 writes | rides in the database: 2
unit-3 started and copied the grid: ['R1', 'R2'] | units running: ['unit-1', 'unit-3']

6. Try it

The write-behind window

During the surge, rides change fast. Choose how often the data writer flushes, how many copies of each item the grid keeps, and how many units fail at once.

Ride changes a second
Flush to the database every
Copies of each item in memory
Units that fail together

Simplified: the database receives one batch per flush; data still waiting for a flush is lost only if every in-memory copy of it is lost.

7. What it costs, and when to pick another style

What you get

  • Handles extreme spikes: the database is off the request path.
  • Elastic: units start and stop in seconds.
  • Fast: reads and writes hit memory.
  • Database load is smoothed: batched writes instead of one per request.

What you pay

  • Complex: replication, conflict rules, write-behind, recovery.
  • Memory-bound: only the hot data fits.
  • A window of possible loss: unflushed writes in memory.
  • Hard to test: behaviour under real concurrency and failure is difficult to reproduce.
If…Consider instead
Load is steady and moderateTiers with a cache and read replicas
Every write must be durable before replyingA database-backed design (payments, bookings)
Spikes are short and the work can waitQueue it: event-driven or serverless
A failure must never reach most customersCell-based

8. Signs you've outgrown it

9. Remember it and say it

Memory hook: "Gate checkers with a shared radio list"Copies of the app serve everything from replicated memory; the database catches up later.
How this hook works: what “Gate checkers with a shared radio list” means

What the slogan means, phrase by phrase

“Gate checkers”
Processing units, as many as the rush needs.
“a shared radio list”
The replicated in-memory data grid.
“the office gets it later”
Write-behind to the database.

Why this picture fits: Match-day ticket checkers share a live list by radio; the head office gets the full list after the match.

The story → the concept

The stadiumPedal
CheckersProcessing units
The radio listThe grid of open rides
Head officeThe database, written in batches
A checker leavingunit-2 crashing; unit-3 copies the list

Watch it on an example

  1. unit-2 sees Mia's ride the moment unit-1 creates it.
  2. The database hears about both rides in one batch.
  3. unit-3 starts by copying the grid; nothing was lost.

The trap it saves you from

Forgetting that unflushed writes live only in memory.

It still works when…

Partially: many systems put only their hottest path (seat holds, bike availability) in a grid.

In an interview or design review

10. Check yourself

Where does a space-based system serve requests from?

The database is updated later by a data writer.

With two in-memory copies of each item and a 5-second flush, when are writes lost?

Replication covers single failures; the flush interval bounds the window.

Which workload suits space-based architecture?

Concerts on-sales, auctions, city-wide surges.

Why can new processing units start quickly?

Units are disposable copies.

11. Go deeper

ResourceWhy
Architecture 15: Space-based and cachingHow an architect evaluates the style.
System design D24: Flash-sale checkoutA surge problem where hot data lives in memory.
Hazelcast documentationA data grid with replication and write-behind map stores.