Under the Hood

High-level architecture: the whole system on one page

Before the deep-dives, the map: what Fable is made of and how a request flows through it. A React Native app talks REST + WebSockets to a single NestJS backend on one Mumbai VM, which fronts a managed Postgres, a co-located Redis, and Cloudflare R2 for media — with Firebase for identity and FCM for push. This chapter is the boxes-and-arrows view and the honest label for the shape: a modular monolith on a single node, chosen deliberately, with every component justified by a requirement from the previous chapter.

High-level architecture: the whole system on one page

With the requirements and constraints in hand, here's the system they produced — the map you'd draw first in an interview, and the one to hold in your head while reading the deep-dive chapters. The guiding principle, forced by the one-developer constraint, is as few moving parts as the requirements allow, and the shape that produced has an honest name: a modular monolith on a single node.

The components

  • Client — a React Native (Expo) mobile app. It holds optimistic local state, generates ULIDs and idempotency keys before it talks to the server, and speaks two protocols to the backend: REST for request/response and WebSockets (Socket.IO) for realtime.
  • Backend — a single NestJS application (TypeScript). It's a monolith — one deployable process — but internally modular, organized into feature modules (auth, groups, expenses, settlements, chat, media, inbox, moderation…) with clear boundaries. One process, many well-separated rooms.
  • Databasemanaged PostgreSQL (GCP Cloud SQL, Mumbai). The single source of truth for everything: the ledger, users, groups, chat messages, all of it. Chosen managed so someone else owns backups and failover.
  • Cache & realtime backplaneRedis, self-hosted next to the API. It does triple duty: the cache/idempotency store, the session-revocation set, and the Socket.IO pub/sub adapter that lets realtime scale across processes.
  • Object storageCloudflare R2 for media (receipts, photos), served through a CDN, accessed via presigned URLs so bytes never flow through the API.
  • IdentityFirebase Auth issues identity (phone OTP, Google); Fable verifies the Firebase token and mints its own session tokens.
  • PushFirebase Cloud Messaging (FCM) delivers push notifications to devices.

The deployment: one VM, one Compose stack

Physically, it's one virtual machine in Mumbai running a single Docker Compose stack, fronted by Caddy (TLS termination + reverse proxy), with the NestJS API container, a cloud-sql-proxy sidecar (the keyless, service-account-authenticated tunnel to Cloud SQL), and the Redis container. The API talks to Postgres through the proxy over a private Docker network; it talks to Redis over the same network; it talks to R2 and FCM over the internet. That's the entire production footprint — described in full in the infra-evolution chapter.

Following a request through the system

Two flows show how the pieces fit.

Adding an expense (REST write):

  1. The app optimistically shows the expense, having minted its ULID and idempotency key locally.
  2. POST /v1/expenses (with Authorization: Bearer <JWT> and Idempotency-Key) hits Caddy, which terminates TLS and reverse-proxies to the NestJS process.
  3. The auth guard verifies the JWT signature and checks the Redis revocation set; the idempotency middleware checks Redis for a replay.
  4. The expenses module validates membership, computes the splits via @hisaab/money, and writes the expense + splits + payers in one Postgres transaction through the cloud-sql-proxy, invalidating the balance cache.
  5. It emits a realtime event so other members' devices update, and enqueues an inbox/push notification.
  6. 201 returns (recorded in the idempotency store for replay).

Receiving a chat message (WebSocket): the sender's message is persisted first, then broadcast to the group's Socket.IO room — routed, if the recipient is on a different process, through the Redis adapter — and delivered down the recipient's socket; offline recipients reconcile via cursor-paginated history on reconnect.

Why a monolith, honestly

The instinct in a system-design interview is to draw microservices. Fable is deliberately not microservices, and the reasoning is the requirements chapter's discipline applied to topology:

  • One developer can't operate a fleet of services — the operational cost (deployment, inter-service networking, distributed tracing, the dual-write/outbox problem) is enormous and buys nothing at Fable's scale.
  • A monolith gets real transactions for free. Because expenses, settlements, and balances live in one database in one process, a money operation is a single ACID transaction with a local lock — no sagas, no 2PC, no distributed consistency to get wrong. Splitting the ledger across services would manufacture the hardest problems in this curriculum for no benefit.
  • Modular internally means the boundaries that matter (feature modules) exist for code clarity, without paying the network and consistency tax of making them physical.

Planned (TDD)

A conventional cloud architecture, roughly along microservice/serverless lines, with managed data services on free tiers.

Shipped

A modular-monolith NestJS process on a single Mumbai VM (Docker Compose: Caddy + api + cloud-sql-proxy + redis), managed Cloud SQL, self-hosted Redis, R2 for media, Firebase for identity, FCM for push.

Every split-out service would cost a solo developer operational time and manufacture distributed-systems problems (dual writes, sagas, cross-service consistency) that a single process with one database avoids entirely. The monolith is the simplest thing that meets the requirements, and it keeps money operations as local ACID transactions.

Interview takeaway

  • Draw the whole system at box altitude first, then walk one request end to end. "Client → TLS-terminating proxy → app → cache-check → DB transaction → emit event → enqueue notification → respond" is exactly the high-level-design step, and it gives you and the interviewer a shared map before any deep-dive.
  • Defend the monolith when it's right. Saying "one service, one database, because it keeps money operations as local transactions and a small team can actually operate it — I'd extract a service only when a specific part needs independent scaling" is a stronger answer than reflexive microservices, and it signals you know what distribution costs.
  • Name what each component is for. Every box in Fable maps to a requirement; a box you can't justify is a box you shouldn't draw.

The chapters from here zoom into the boxes: the data model and expense engine inside the database, auth and chat and media inside the app, and the infra under it all.

Go deeper

Check yourself

Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.

  1. List Fable's major components and state what each one is for. Which single component does triple duty, and what are its three jobs?
  2. Walk an "add expense" request from the app through TLS termination, auth, idempotency, the database transaction, and the notification — naming the component at each step.
  3. Fable is a modular monolith, not microservices. Give the strongest single technical reason (about money operations and transactions), plus the operational reason tied to the team constraint.
  4. What does "modular internally, monolithic in deployment" mean, and what tax does it avoid compared to making those module boundaries physical services?
  5. The production footprint is one Compose stack. Name the four containers and what each does, and how the API reaches the managed database.
  6. In an interview, why can "one service, one database, extract later if needed" be a stronger answer than microservices, and what would justify extracting a service?