InterviewKata
Architecture styles · 08 of 16 · Across machines

Microservices

Microservices trade the monolith's simplicity for independence: many teams releasing and scaling their own parts. This page builds Pedal as three services with their own data, keeps billing off the rider's path with an event, survives billing being down, and shows in a lab why long synchronous call chains are the style's quiet killer.

1. In one picture

A food market has many small stalls, each run by its own owner with its own kitchen, stock and opening hours. One stall can change its menu, close for a day or double its staff without asking the others. The market works as long as the stalls agree on a few things: where customers queue, how they pay, and that nobody takes ingredients from another stall's fridge.

Microservices split a system into small services, each built around one business capability, owned by one team, deployed on its own and owning its own data: no other service reads its database. Services talk through APIs (synchronous calls when the caller needs an answer now) and events (asynchronous messages when it doesn't).

The point is organisational as much as technical: many teams can change, release and scale their parts independently. The price is that everything a monolith gave for free (a function call, a transaction, a stack trace) becomes a network call, a saga and a distributed trace. See Architecture module 16 for the architect's view.

2. Pedal built this way

An unlock needs an answer now, so Rides calls Docks and waits. Billing doesn't need to answer the rider at all, so Rides publishes an event and moves on. Follow a return while Billing is down.

Two habits make this safe: idempotent consumers (Billing remembers which events it has handled, so a redelivery does nothing) and events written reliably (with an outbox, so a ride can't be saved without its event: System design module 22).

3. The rules

RuleWhy it existsBroken when…
One service, one team, one business capabilityA team can change its service without negotiating with others.One feature needs coordinated releases of four services.
Each service owns its dataSchema changes stay private; services can pick the right store.Billing queries the rides database directly.
Independent deploymentReleases don't queue behind each other.Services must be released in a fixed order on the same day.
Prefer events when nobody needs to waitFewer synchronous chains, fewer cascading failures.Unlock waits for billing, analytics and email to answer.
Design for failureEvery call can be slow or fail.No timeouts, no retries with backoff, no circuit breakers.

4. How it goes wrong

The distributed monolithServices that must be deployed together and share a database: all the costs, none of the benefits.
Too smallServices split by entity or function (a service per table), so every request crosses five services.
Long synchronous chainsA request that calls A → B → C → D: availabilities multiply and latencies add.
No observabilityWithout trace IDs across services, a slow request can't be traced to its cause (System design 25).

5. The code

Rides, Docks and Billing as separate services, each with its own store. Unlock calls Docks synchronously; return publishes an event that Billing consumes later. Billing is down at first, then catches up, and ignores a redelivered duplicate.

from __future__ import annotations
from collections import deque

# Three services, each deployed alone and owning its own data. Here they're objects; in production, processes.


class DocksService:
    def __init__(self) -> None:
        self.store = {"D12": ["B7", "B9"]}            # only Docks reads or writes this

    def release(self, dock: str) -> str:             # a synchronous API: the caller waits
        return self.store[dock].pop(0)


class Queue:                                         # a message broker between services
    def __init__(self) -> None:
        self.messages: deque[dict] = deque()

    def publish(self, msg: dict) -> None:
        self.messages.append(msg)


class BillingService:
    def __init__(self) -> None:
        self.store: dict[str, int] = {}               # charges by event id: only Billing owns this
        self.up = True

    def consume(self, queue: Queue) -> None:
        while queue.messages and self.up:
            msg = queue.messages.popleft()
            if msg["id"] in self.store:              # idempotent: a redelivered event changes nothing
                print(f"duplicate delivery of {msg['id']} ignored")
                continue
            pence = max(0, msg["minutes"] - 30) * 10 if msg["plan"] == "member" else 100 + msg["minutes"] * 10
            self.store[msg["id"]] = pence
            print(f"Billing charged {msg['rider']} £{pence // 100}.{pence % 100:02d} for {msg['id']}")


class RidesService:
    def __init__(self, docks: DocksService, queue: Queue) -> None:
        self.docks, self.queue = docks, queue
        self.store: dict[str, dict] = {}              # only Rides owns this

    def unlock(self, rider: str, plan: str, dock: str) -> str:
        bike = self.docks.release(dock)              # needs an answer now: synchronous call
        ride_id = f"R{len(self.store) + 1}"
        self.store[ride_id] = {"rider": rider, "plan": plan, "bike": bike}
        print(f"Rides -> Docks (sync): released {bike} at {dock}")
        return ride_id

    def give_back(self, ride_id: str, minutes: int) -> None:
        ride = self.store[ride_id]
        event = {"id": f"RideEnded#{ride_id}", "rider": ride["rider"], "plan": ride["plan"], "minutes": minutes}
        self.queue.publish(event)                    # nobody needs to wait for billing: an event
        print(f"Rides published {event['id']} ({ride['rider']}, {minutes} min)")


if __name__ == "__main__":
    queue, docks, billing = Queue(), DocksService(), BillingService()
    rides = RidesService(docks, queue)
    r1 = rides.unlock("Mia", "member", "D12")
    billing.up = False
    rides.give_back(r1, 42)
    billing.consume(queue)
    print(f"Billing is down: {len(queue.messages)} event waiting in the queue")
    billing.up = True
    billing.consume(queue)
    queue.publish({"id": "RideEnded#R1", "rider": "Mia", "plan": "member", "minutes": 42})   # the broker redelivers
    billing.consume(queue)
    print(f"each service owns its data: rides={len(rides.store)}, bikes at D12={len(docks.store['D12'])}, charges={len(billing.store)}")
// Three services, each deployed alone and owning its own data. Here they're objects; in production, processes.
interface Msg { id: string; rider: string; plan: string; minutes: number; }

class DocksService {
  store: Record<string, string[]> = { D12: ["B7", "B9"] }; // only Docks reads or writes this
  release(dock: string): string { return this.store[dock].shift() as string; } // a synchronous API: the caller waits
}

class Queue { messages: Msg[] = []; publish(m: Msg): void { this.messages.push(m); } } // a message broker between services

class BillingService {
  store = new Map<string, number>(); // charges by event id: only Billing owns this
  up = true;
  consume(q: Queue): void {
    while (q.messages.length && this.up) {
      const m = q.messages.shift() as Msg;
      if (this.store.has(m.id)) { console.log(`duplicate delivery of ${m.id} ignored`); continue; } // idempotent
      const pence = m.plan === "member" ? Math.max(0, m.minutes - 30) * 10 : 100 + m.minutes * 10;
      this.store.set(m.id, pence);
      console.log(`Billing charged ${m.rider} £${Math.floor(pence / 100)}.${String(pence % 100).padStart(2, "0")} for ${m.id}`);
    }
  }
}

class RidesService {
  store = new Map<string, { rider: string; plan: string; bike: string }>(); // only Rides owns this
  constructor(private docks: DocksService, private queue: Queue) {}
  unlock(rider: string, plan: string, dock: string): string {
    const bike = this.docks.release(dock); // needs an answer now: synchronous call
    const id = `R${this.store.size + 1}`;
    this.store.set(id, { rider, plan, bike });
    console.log(`Rides -> Docks (sync): released ${bike} at ${dock}`);
    return id;
  }
  giveBack(id: string, minutes: number): void {
    const ride = this.store.get(id) as { rider: string; plan: string; bike: string };
    const event: Msg = { id: `RideEnded#${id}`, rider: ride.rider, plan: ride.plan, minutes };
    this.queue.publish(event); // nobody needs to wait for billing: an event
    console.log(`Rides published ${event.id} (${ride.rider}, ${minutes} min)`);
  }
}

const queue = new Queue(), docks = new DocksService(), billing = new BillingService();
const rides = new RidesService(docks, queue);
const r1 = rides.unlock("Mia", "member", "D12");
billing.up = false;
rides.giveBack(r1, 42);
billing.consume(queue);
console.log(`Billing is down: ${queue.messages.length} event waiting in the queue`);
billing.up = true;
billing.consume(queue);
queue.publish({ id: "RideEnded#R1", rider: "Mia", plan: "member", minutes: 42 }); // the broker redelivers
billing.consume(queue);
console.log(`each service owns its data: rides=${rides.store.size}, bikes at D12=${docks.store.D12.length}, charges=${billing.store.size}`);
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

// Three services, each deployed alone and owning its own data. Here they're objects; in production, processes.
record Msg(String id, String rider, String plan, int minutes) {}

class DocksService {
    final Map<String, List<String>> store = new HashMap<>(Map.of("D12", new ArrayList<>(List.of("B7", "B9")))); // only Docks reads or writes this
    String release(String dock) { return store.get(dock).remove(0); } // a synchronous API: the caller waits
}

class Queue { final Deque<Msg> messages = new ArrayDeque<>(); void publish(Msg m) { messages.add(m); } } // a message broker between services

class BillingService {
    final Map<String, Integer> store = new LinkedHashMap<>(); // charges by event id: only Billing owns this
    boolean up = true;
    void consume(Queue q) {
        while (!q.messages.isEmpty() && up) {
            Msg m = q.messages.poll();
            if (store.containsKey(m.id())) { System.out.println("duplicate delivery of " + m.id() + " ignored"); continue; } // idempotent
            int pence = m.plan().equals("member") ? Math.max(0, m.minutes() - 30) * 10 : 100 + m.minutes() * 10;
            store.put(m.id(), pence);
            System.out.println("Billing charged " + m.rider() + " £" + (pence / 100) + "." + String.format("%02d", pence % 100) + " for " + m.id());
        }
    }
}

class RidesService {
    private final DocksService docks; private final Queue queue;
    final Map<String, String[]> store = new LinkedHashMap<>(); // only Rides owns this
    RidesService(DocksService docks, Queue queue) { this.docks = docks; this.queue = queue; }
    String unlock(String rider, String plan, String dock) {
        String bike = docks.release(dock); // needs an answer now: synchronous call
        String id = "R" + (store.size() + 1);
        store.put(id, new String[]{rider, plan, bike});
        System.out.println("Rides -> Docks (sync): released " + bike + " at " + dock);
        return id;
    }
    void giveBack(String id, int minutes) {
        String[] ride = store.get(id);
        Msg event = new Msg("RideEnded#" + id, ride[0], ride[1], minutes);
        queue.publish(event); // nobody needs to wait for billing: an event
        System.out.println("Rides published " + event.id() + " (" + ride[0] + ", " + minutes + " min)");
    }
}

public class Main {
    public static void main(String[] args) {
        Queue queue = new Queue(); DocksService docks = new DocksService(); BillingService billing = new BillingService();
        RidesService rides = new RidesService(docks, queue);
        String r1 = rides.unlock("Mia", "member", "D12");
        billing.up = false;
        rides.giveBack(r1, 42);
        billing.consume(queue);
        System.out.println("Billing is down: " + queue.messages.size() + " event waiting in the queue");
        billing.up = true;
        billing.consume(queue);
        queue.publish(new Msg("RideEnded#R1", "Mia", "member", 42)); // the broker redelivers
        billing.consume(queue);
        System.out.println("each service owns its data: rides=" + rides.store.size() + ", bikes at D12=" + docks.store.get("D12").size() + ", charges=" + billing.store.size());
    }
}

Output (the same in all three languages)

Rides -> Docks (sync): released B7 at D12
Rides published RideEnded#R1 (Mia, 42 min)
Billing is down: 1 event waiting in the queue
Billing charged Mia £1.20 for RideEnded#R1
duplicate delivery of RideEnded#R1 ignored
each service owns its data: rides=1, bikes at D12=1, charges=1

6. Try it

The call chain

Each service in a synchronous chain must be up for the request to succeed, and each adds latency. Set the chain and see what the rider gets.

Services called in a chain
Availability of each
Typical latency of each (ms)
Calls made asynchronous (events)

Simplified: failures are independent, latency adds up along the chain, and asynchronous steps leave the rider's path (they still have to succeed eventually, through retries).

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

What you get

  • Independent teams: each releases on its own schedule.
  • Independent scaling: only the busy service gets more copies.
  • Fault isolation: a broken report service doesn't stop unlocking.
  • Technology choice per service: a store or language that fits each job.

What you pay

  • Distributed everything: network calls, partial failures, retries, timeouts.
  • No cross-service transactions: sagas and eventual consistency instead.
  • Operational load: dozens of deployables, pipelines, dashboards and on-call rotations.
  • Harder debugging: a request's story is spread over many logs.
If…Consider instead
Few teams, or boundaries still unclearModular monolith, then extract
A handful of business areas, shared data is fineService-based
Mostly reacting to events with spiky loadServerless functions
A failure must never reach all customersAdd cells around the services

8. Signs you've outgrown it

9. Remember it and say it

Memory hook: "Stalls in a market"Small services, each owned by one team, deployed alone, with their own data, cooperating through APIs and events.
How this hook works: what “Stalls in a market” means

What the slogan means, phrase by phrase

“Stalls”
Services around business capabilities.
“in a market”
Shared rules for how they talk, never shared fridges (databases).

Why this picture fits: A food market: each stall has its own owner, kitchen and stock; they share the queue and the payment rules.

The story → the concept

The marketPedal
StallsRides, Docks and Billing services
Each stall's fridgeEach service's own store
Shouting an order acrossA synchronous call: Rides asks Docks
The noticeboardEvents: RideEnded

Watch it on an example

  1. Unlock waits for Docks, because the rider needs a bike now.
  2. Return publishes an event; Billing charges when it's back up.
  3. A redelivered event is ignored, so nobody is charged twice.

The trap it saves you from

Splitting into services before there are teams and boundaries to justify them.

It still works when…

With cells, serverless or hexagonal insides: microservices combine with most other styles.

In an interview or design review

10. Check yourself

Billing reads the Rides database directly to save an API call. What's the problem?

Each service owns its data; others ask it or listen to its events.

Four services in a synchronous chain are each 99.9% available. Roughly how available is the chain?

0.999⁴ ≈ 0.996: availabilities multiply.

Why does Billing keep a list of event IDs it has handled?

Brokers deliver at least once; consumers must be idempotent.

Which is the strongest reason to adopt microservices?

It's largely an organisational choice.

11. Go deeper

ResourceWhy
Architecture 16: MicroservicesHow an architect weighs the style.
Microservices (James Lewis and Martin Fowler)The article that named the style.
System design 24: Reliability patternsTimeouts, retries, breakers and bulkheads in depth.
Case study: Segment back to a monolithWhen a team undid its microservices.