Skip to content

An example system architecture document

Example

Lidoline is a made-up booking platform for lidos and outdoor pools. This is the document we’d hand over before anyone writes code: the system, the decisions, the options we ruled out, the cost picture and how it fails.

Example. Lidoline is fictional. Names, dates, loads and costs are invented to show the shape of the work. The costs are the made-up client’s own hosting bills, not our fees.

What it’s for

A system architecture document says what a system is made of, why each part was chosen, and what happens when parts of it fail, before anyone builds it. A good one covers the services and their boundaries, the data model, hosting and cost at today’s load and the planned one, observability, failure modes, and how the system grows, along with the options that were ruled out.

Below is a complete example for Lidoline, a fictional booking platform, written the way we’d deliver it.

Context and requirements

Document
Lidoline: system architecture
Version
1.0, final
Date
16 October 2026
For
Lidoline Ltd (Jess Okafor, founder; Sam Reyes, product)

Status: Reviewed with Jess and Sam on 14 October. Changes from that review are listed under section 11.

1Context

Lidoline sells session booking to lidos and outdoor pools. Each venue is a customer (a “tenant”): some are council-run, most are community trusts. Swimmers book a session, pay, and show a QR code at the gate. Venue staff manage sessions, capacity, memberships and closures.

Today Lidoline runs on a no-code booking tool with a custom front end. It works for the four pilot venues. It will not work for the 40 venues signed for the 2027 season, for three reasons:

  1. The hot-morning spike. Venues release the next seven days of sessions at 7am. On the first hot day of 2026 one pilot venue had 1,100 people trying to book 240 places in the first five minutes. The current tool double-sold 31 places.
  2. Closures. When a venue closes for weather or water quality, every booking for the session has to be cancelled, refunded and told, quickly. Today staff do this by hand.
  3. Money. Each venue must be paid directly. Lidoline takes a booking fee and must never hold venue money itself.

2Requirements

Must

  • Never sell more places than a session holds, under any load.
  • Take payment on behalf of each venue, with Lidoline’s fee split out at the time of payment.
  • Cancel, refund and notify every booking in a closed session within 15 minutes of staff pressing “close”.
  • Let gate staff check people in when the venue’s Wi-Fi drops, which it does.
  • Keep each venue’s data separate from every other venue’s.

Should

  • Handle the 7am release for 40 venues at once, at five times the worst pilot spike.
  • Be run and changed by a small team. Lidoline plans to hire two developers in 2027.

Won’t (this version)

  • Lessons and courses. Planned for 2028.
  • A native app. The booking site works on phones and saves the QR code to the wallet.

The system

3The system at a glance

Four tiers, left to right: who uses it, the applications, the data, and the infrastructure and outside services. The booking hold (Redis) is the part that carries the hot-morning problem.

Example: a fictional booking platform, with place holds carrying the 7am rush.IN LIDOLINE’S AWSSwimmersphone browsersGate check-inworks offline, syncslaterVenue staffdashboardBooking siteNext.jsBooking APIone serviceJob workerrefunds, messagesRedisplace holdsholdsPostgreSQLbookings, tenantsJob queueSQSObservabilitytraces, metrics, logsAWSECS, RDSStripe Connectpays each venueEmail and SMS
Example: a fictional booking platform, with place holds carrying the 7am rush.

4Services and boundaries

One booking API, not several services. Bookings, sessions, memberships, closures and payments live in one TypeScript service with clear modules inside it. The modules talk through function calls and their own tables, never each other’s tables, so any one of them can be split out later if it needs to be.

Services and boundaries
PartDoesDoesn’t
Booking site (Next.js)Public booking pages, venue dashboard, account pages. Renders session lists on the server so they’re fast on phones.Hold any business rules. Every rule lives in the API.
Booking APISessions, capacity, holds, bookings, memberships, closures, Stripe payment set-up, webhooks from Stripe.Send messages or refunds itself. It puts a job on the queue.
Job workerRefunds, emails, texts, the 7am release, nightly reports to venues. Retries each job with backoff.Take requests from the outside world.
Gate check-inA web app installed on staff tablets. Downloads the day’s bookings each morning, checks QR codes locally, syncs check-ins when it can.Need a connection to let someone in.

5Data model and storage

PostgreSQL holds everything that matters: tenants, venues, sessions, bookings, payments, memberships, check-ins. Every table that holds venue data carries a tenant_id, and row-level security in Postgres enforces it, so a bug in one query can’t show one venue another venue’s swimmers.

Core tables
TableKey fieldsNotes
tenantsid, name, stripe_account_idOne per venue operator
venuesid, tenant_id, name, timezoneA trust may run more than one pool
sessionsid, venue_id, starts_at, capacity, statusStatus: open, full, closed, cancelled
bookingsid, session_id, swimmer_id, places, status, payment_intent_idUnique on payment_intent_id, so a retried webhook can’t book twice
holdssession_id, hold_id, places, expires_atIn Redis, not Postgres. See below
check_insbooking_id, device_id, checked_in_atWritten by the gate app on sync; duplicates ignored

Redis holds places while a swimmer pays. Taking a hold is one atomic operation: decrement the session’s available count if it’s high enough, otherwise refuse. Holds expire after eight minutes. Postgres is the record; Redis is the turnstile. If Redis and Postgres ever disagree, Postgres wins and a nightly job reconciles the counts.

Why not DynamoDB: see decision D2.

Running it

6Hosting and cost

Booking site on Vercel. API, worker, Postgres (RDS), Redis (ElastiCache) and the queue (SQS) in one AWS account owned by Lidoline, in eu-west-2 (London). Estimated monthly running costs, before Stripe’s per-transaction fees:

Estimated monthly running costs
Item2027 season (40 venues)At 10 times that
Vercel (booking site)Under £100£100 to £300
API and worker (ECS Fargate, two small tasks each, more at 7am)£50 to £150£300 to £600
PostgreSQL (RDS, Multi-AZ)£150 to £250£400 to £800
Redis (ElastiCache, small)Under £50£50 to £150
Queue, email and SMSMostly SMS: depends on how many venues turn it onSame
ObservabilityUnder £50£100 to £300

The biggest cost lever is SMS, not hosting. Closure messages go by email by default and by text only where the venue pays for it.

7Observability

  • Traces on every API request and job, with tenant_id and session_id attached, sent through OpenTelemetry so the vendor can change without touching code.
  • Metrics that matter to the business, not just the servers: holds taken per minute, holds expired, bookings per minute, refund jobs waiting, minutes since a closure was pressed.
  • Alerts on: any oversell (should be impossible, so it pages), refund jobs waiting more than 10 minutes, Stripe webhook failures, error rate above 2% for five minutes.
  • Logs structured as JSON, kept for 30 days, with no card or health data in them.

8Failure modes

Failure modes
What failsWhat swimmers seeWhat the system does
7am spike beyond the planA short queue page (“you’re in line”), then booking as normalThe site admits people to booking at the rate the API can serve; holds stop overselling regardless of load
Stripe is down“Payments are paused, your place is held for eight minutes”Holds expire normally; nothing is booked without a confirmed payment
Stripe webhook arrives twice, or lateNothingBookings are unique on the payment ID; late webhooks still confirm if the hold has lapsed but the place is free, otherwise refund automatically
Redis is lostBooking pauses for a minute or twoAPI falls back to a Postgres row lock per session: slower, still correct. Redis counts rebuild from Postgres on restart
A venue closes a sessionEmail (and text if enabled) within 15 minutes, refund to the cardOne job per booking on the queue; the worker retries failures; staff see a live count of refunds done and waiting
Gate Wi-Fi dropsNothingGate app checks QR codes against its morning download; syncs later; duplicate check-ins are ignored
Email or SMS provider is downMessage arrives lateJobs retry for six hours, then alert
Whole AWS region is downBooking site shows a status pageAccepted risk for v1. Nightly database snapshots copied to a second region; restore runbook tested before the season

9How it grows

  • First to strain: the 7am release. At 40 venues on one hot morning, the plan is five times the worst pilot spike. The holds design handles that; the queue page is the safety valve beyond it.
  • At ten times the venues: Postgres moves up one instance size, the API scales out (it’s stateless), and the 7am release staggers by a few minutes per venue, which venues won’t notice and the system will.
  • What can wait: read replicas, splitting out the payments module, a second region. None of them is needed for 2027, and each has a clear trigger written down in section 11.

Decisions and build order

10What to build first

  1. Sessions, holds and bookings with Stripe Connect, end to end, for one venue. Load test the hold against 5,000 simultaneous attempts on 240 places before anything else is built.
  2. Closures, refunds and messages.
  3. The venue dashboard.
  4. Gate check-in.
  5. Memberships.
  6. Migration of the four pilot venues, then onboarding the rest in batches of ten.

11Decisions and options ruled out

D1One service, not microservices

Chosen
One booking API with strict modules inside it.
Ruled out
Separate services for bookings, payments, memberships and messages.
Why
A team of one to three developers can’t run six services well. One service with strict modules gives the same boundaries without the network between them.
Revisit if
One module needs to deploy on its own schedule, or more than six people work on the API.

D2PostgreSQL, not DynamoDB

Chosen
PostgreSQL on RDS as the record for everything.
Ruled out
DynamoDB.
Why
Bookings, payments and refunds need transactions across several records, and venues will want reports Lidoline hasn’t thought of yet. Postgres does both. DynamoDB would handle the 7am spike well but makes the rest harder.
Revisit if
Never, for this product, unless write volume grows a hundredfold.

D3Redis holds, not database locks alone

Chosen
Places held in Redis with one atomic operation, with Postgres row locks kept as the fallback (section 8).
Ruled out
Locking session rows in Postgres on its own.
Why
Row locks are correct but queue every swimmer behind one row at 7am. Redis takes the hold in microseconds.

D4Stripe Connect, not collecting and paying out

Chosen
Stripe Connect: venues are paid directly and Lidoline’s fee is split at the moment of payment.
Ruled out
Collecting into one Lidoline account and paying venues weekly.
Why
Lidoline never holds venue money. Collecting and paying out brings safeguarding obligations Lidoline doesn’t want.

D5A web app for gate check-in, not a native app

Chosen
A web app installed on the venue’s own tablets, working offline once loaded.
Ruled out
A native tablet app.
Why
It installs on any tablet the venue already has, with no app store release to manage.
Revisit if
Venues want contactless card scanning at the gate, which needs native access.

Deferred, and what brings each back

  • Read replicas: when database CPU stays above 60% through the 7am release, or venue reports start to slow bookings down.
  • Splitting out the payments module: the trigger in decision D1.
  • A second region: when a venue contract requires booking to keep running through a regional outage, which the snapshot restore in section 8 can’t do.

Changed after the 14 October review

  • Closure messages default to email, with SMS optional per venue (Jess, on cost).
  • Added the queue page for the 7am release (Sam, from the 2026 pilot complaints).

12Open questions

  • Will councils require their venues’ data to stay in a UK region? Assumed yes; eu-west-2 covers it.
  • Do any venues need to take cash at the gate? If so, the gate app needs a “walk-in” booking, which isn’t designed yet.

Have something to build?

Tell us what you're working on. We'll tell you how we'd approach it.

We reply within one working day.