Time to use this whole module on a real brief. No servers or scaling yet: just finding the requirements, sizing the problem, and making the first decisions with reasons.
Here's the brief, as it arrived in a voice note:
"Hi, I'm Kavya, I run Smash Point, a badminton club. Eight courts, about 1,200 members. Every Sunday at 8pm we release next week's slots in our WhatsApp group, and it's chaos. Evening slots are gone in two minutes, people claim the same court, and some members grab six slots while others get none. Can you build us a booking system?"
Step 1: Find the requirements
Write the requirements
Before reading on, list the functional requirements for members and for Kavya's staff, then the non-functional ones. The voice note hints at at least three non-functional requirements without naming them.
Show one answer
Members: see a week's grid of courts and hours, book a slot, cancel, see their own bookings.
Staff: block courts for coaching or tournaments, see the day's bookings, mark no-shows.
Hidden non-functional requirements in the voice note:
- "People claim the same court": a slot can never be booked twice.
- "Some members grab six slots": a per-member limit, enforced strictly.
- "Gone in two minutes": the system must stay up and fast during the Sunday release.
A follow-up call with Kavya pins down the details:
- Courts open 6am to 11pm, in 1-hour slots: 8 courts × 17 hours × 7 days = 952 slots a week.
- Each member may hold at most 3 bookings per week.
- Cancellation is allowed until 6 hours before. The slot goes back into the pool.
- Members' phone numbers must not be visible to other members.
- Out of scope for version one: payments (fees are collected monthly), coaching sessions, guest players.
| Requirement | Category | Target |
|---|---|---|
| A slot is never booked twice | Consistency | Always |
| No member ever holds more than 3 bookings in a week | Consistency | Always, even with simultaneous taps |
| Booking works during the Sunday 8pm release | Availability, performance | Responses under 1 s for the first 10 minutes |
| Members' contact details stay private | Security | Always |
| Bookings are never lost | Durability | Daily backups, tested |
Step 2: Size it
Most of the week is quiet: maybe 150 bookings and cancellations a day. The interesting moment is Sunday at 8:00:00pm.
How big is the rush?
Say 700 members are online at 8pm. In the first 10 seconds, each loads the grid once and makes about 3 booking attempts (some fail because the slot is gone, so they try another). What's the request rate during those 10 seconds?
Show one answer
700 × (1 grid load + 3 attempts) = 2,800 requests in 10 seconds, or about 280 requests a second. After a minute it falls back to a trickle.
280 requests a second is modest. One ordinary server and one Postgres database can handle it, if each request is cheap. So the numbers give us two decisions:
- One server, one database. No cache, no queue, no load balancer.
- Keep the 8pm path lean. A booking request should be one short database transaction. Anything slow, like sending a WhatsApp confirmation, happens after the booking commits, never inside it.
Step 3: High-level design
%% caption: Smash Point's high-level design. The database is highlighted because it enforces both hard rules.
flowchart TD
MP(["Members' phones"]) --> W
SD(["Staff dashboard"]) --> W
W["Web app<br/>grid, bookings, admin"] --> D[("Database<br/>courts, slots, bookings")]:::hot
W -- "confirmations" --> N["WhatsApp/SMS provider"]Two decisions are worth stating:
- The release time is enforced by the server's clock, not the app. If the app simply hides the Book button until 8pm, someone will find a way to send the request at 7:59. The server rejects bookings for next week until 20:00:00 by its own clock.
- Confirmations go out after the commit. If the messaging provider is slow at 8pm, members still get their slots. They just get the message a few seconds later.
Step 4: The two hard rules
Rule 1: no double bookings
Two members tap the same 7pm court-3 slot in the same millisecond. Checking "is it free?" in application code and then inserting isn't safe: both requests can see "free" and both insert. Let the database enforce it, where it can't be raced:
CREATE UNIQUE INDEX one_booking_per_slot
ON bookings (court_id, starts_at)
WHERE status = 'booked';
The second insert fails with a unique-violation error, and the app shows "Just taken. Here are the nearest free slots." The WHERE status = 'booked' part lets a cancelled booking stay in the table for history without blocking the slot.
Rule 2: at most 3 per member per week
This one is sneakier. A unique index can't count. And a member with two phones (or one fast thumb) can send two bookings for different courts at the same time.
Close the race
Member 512 already has 2 bookings this week. Two requests arrive together, for court 1 at 7pm and court 5 at 8pm. Each request runs "count my bookings this week, and insert if the count is below 3". What goes wrong, and how do you fix it?
Show one answer
Both requests count 2, both decide "under the limit", and both insert. The member now has 4.
The fix is to give the two requests one thing they must both change, so the database orders them. Keep a small counter row per member per week, and claim a unit from it with a conditional update in the same transaction as the booking (shown below). The second request waits for the first to commit, then finds the count at 3 and updates zero rows.
BEGIN;
-- claim one of the member's 3 weekly bookings
UPDATE weekly_quota
SET used = used + 1
WHERE member_id = :member_id AND week = :week AND used < 3;
-- 0 rows updated? limit reached: ROLLBACK and tell the member
INSERT INTO bookings (court_id, starts_at, member_id, status)
VALUES (:court_id, :starts_at, :member_id, 'booked');
-- unique violation? slot taken: ROLLBACK (this also returns the quota unit)
COMMIT;
Because both steps are in one transaction, a failure in either undoes both. A member never loses a quota unit to a slot they didn't get. (The quota rows for next week are created when the week is released. Cancelling a booking decrements used in the same transaction that cancels it.)
Here's one booking at 8pm as a request flow, with both rules checked inside one short transaction:
%% caption: One booking at 8pm. The confirmation goes out only after the transaction commits.
sequenceDiagram
autonumber
participant P as Member's phone
participant W as Web app
participant D as Database
participant N as Message provider
P->>W: book court 3 at 7pm
W->>D: BEGIN, claim quota
D-->>W: 1 row updated
W->>D: insert the booking
alt slot still free
D-->>W: inserted, COMMIT
W-->>P: booked
W-)N: send the confirmation
else slot already taken
D-->>W: unique violation, ROLLBACK
W-->>P: taken, here are nearby slots
endStep 5: Is first-come-first-served fair?
The design so far makes 8pm a race, and the race favours fast phones and good connections. Kavya might be fine with that. Or she might not.
An alternative for the prime 6pm-10pm slots is a ballot. Members submit up to 3 ranked choices between Sunday 8pm and Monday 8pm. A job then allocates the slots randomly among the people who asked, honouring rankings where it can. Everything else stays first-come-first-served.
| First come, first served | Ballot for prime slots | |
|---|---|---|
| Fairness | Rewards speed | Equal chance for everyone |
| Sunday 8pm load | A sharp spike | Spread over 24 hours |
| Member experience | Instant answer | Wait a day for results |
| Build effort | What we have | An allocation job and a results screen |
This isn't a technical decision. It's a product decision with technical consequences, so it goes back to Kavya with the trade-offs laid out. Version one ships first-come-first-served, and the ballot is written down as an option for version two.
Step 6: State the trade-offs
- One server, no standby. If it dies at 8pm on a Sunday, the release is delayed until it's back. To limit the damage, the release is moved to a scheduled job that can be re-run at a new announced time, and the server is restarted by its host automatically.
- Daily backups only. Up to a day of bookings could be lost in a disaster. Members would have to rebook, but no money is involved, so Kavya accepts this.
- No waitlist. When someone cancels, the slot simply reappears. Whoever looks first gets it.
The final design
%% caption: Version one. Sized for a 280 requests/second spike once a week, with both hard rules enforced by the database.
flowchart TD
MP(["Members' phones"]) --> W
SD(["Staff dashboard"]) --> W
W["Web app<br/>one small server"] --> D[("Database<br/>unique slot index, quota rows")]:::hot
W -. "after commit" .-> N["WhatsApp/SMS provider"]Build it on AWS
Optional on a first read. This section maps the design onto real AWS services and prices, and it uses cloud services the course only introduces later. If you're new to system design, skip it for now: the design above is what an interview asks about. Come back once you've finished Part 1.
Smash Point is the smallest system in this course, and that's exactly why it's worth putting on AWS. Cloud consoles make it easy to click together a load balancer, containers, a Multi-AZ database and a queue, and every one of them would be a mistake here. The skill this lab teaches on AWS is right-sizing: buying what the requirements ask for and saying no to the rest. Prices are Mumbai (ap-south-1) on-demand list prices as of late 2026. They differ from other Regions, so check the AWS pricing calculator before quoting them.
The architecture
%% caption: Smash Point on AWS. One small instance, one small managed database, nothing else.
flowchart TD
MP(["Members' phones"]) --> APP
SD(["Staff dashboard"]) --> APP
APP["One EC2 instance<br/>t4g.small, app and TLS"]:::hot --> DB[("RDS for PostgreSQL<br/>db.t4g.micro, Single-AZ")]:::store
APP -. "after commit" .-> WA["WhatsApp messages<br/>End User Messaging Social"]| Generic box | On AWS | The setting that matters |
|---|---|---|
| Web app, one small server | One EC2 t4g.small (2 vCPU, 2 GiB) running the app and a TLS proxy |
An Elastic IP, and automatic recovery so a failed host brings the instance back |
| Database | RDS for PostgreSQL, db.t4g.micro, Single-AZ |
Automated backups on, with a week or two of retention |
| The server's clock | Amazon Time Sync Service, reachable from every EC2 instance | Time rules checked by the database's clock, in one place |
| WhatsApp/SMS provider | AWS End User Messaging Social, which sends WhatsApp messages and runs in Mumbai | Sent after the booking commits, never inside the transaction |
| Weekly release job | A cron job on the instance | Safe to re-run at a new announced time |
Argument 1: do we need a load balancer, containers or Multi-AZ?
The worst moment is 280 requests a second for about ten seconds, once a week. Everything else is a trickle. So start from the smallest setup that survives that spike, and price what each extra box would buy:
EC2 t4g.small: $0.0112/hour x 730 = ~$8 a month
RDS db.t4g.micro, 1 AZ: $0.021/hour x 730 = ~$15
20 GB of gp3 storage: 20 x $0.131 = ~$3
Public IPv4 address: $0.005/hour x 730 = ~$4
~$30 a month
The spike fits because of how T4g instances work. They launch in unlimited mode by default: they can run above their baseline CPU whenever they need to, and you only pay extra if the 24-hour average stays above the baseline. Ten seconds a week never will. Load-test the instance at 300 requests a second before the first Sunday anyway, since that's cheaper than finding out from Kavya.
Now the boxes you'd be tempted to add:
| Extra | Roughly | What it buys Smash Point |
|---|---|---|
| A load balancer | +$17 a month before traffic | Nothing until there are two app servers |
| A second app instance | +$8, plus the load balancer | Sunday survives one instance dying, but the database is still a single point |
| Multi-AZ database | +$18 | Failover in a minute or two instead of a restore |
| Containers and an orchestrator | Time, mostly | Nothing a club needs |
Each line could double the bill, and the brief already settled the question: Kavya accepted "if the server dies at 8pm, the release is delayed". That acceptance is a requirement. It's what makes the $30 design correct rather than cheap.
Argument 2: daily backups were the requirement. What does RDS actually give you?
The lab accepted losing up to a day of bookings. RDS does better for free. Automated backups take a daily snapshot and upload the transaction logs to S3 every five minutes, so you can restore to any second inside the retention period, losing at most about five minutes of bookings. A database this small stays within the free backup allocation.
Two things to know before you need it. A point-in-time restore creates a new database instance with a new endpoint, so the app's configuration has to change. And the app instance holds nothing worth backing up: rebuild it from a launch template or a setup script. Do one practice restore before the club opens bookings. It turns "we have backups" into "we know it takes this long to get back".
Argument 3: whose clock says 20:00:00?
The lab said the server's clock enforces the release, not the app's. On AWS there's a subtlety: cloud servers usually run on UTC, and Sunday 8pm in India is 14:30 UTC. A release rule written as "hour >= 20" on a UTC machine opens bookings five and a half hours late, or, worse, someone "fixes" it on the app server only.
Put the rule in one place, the database, and compare against an explicit time zone:
INSERT INTO bookings (court_id, starts_at, member_id, status)
SELECT :court_id, :starts_at, :member_id, 'booked'
WHERE now() >= (date_trunc('week', :starts_at AT TIME ZONE 'Asia/Kolkata')
- interval '1 day' + interval '20 hours') AT TIME ZONE 'Asia/Kolkata';
Zero rows inserted means "not open yet": roll back, so the quota unit claimed earlier in the transaction is returned. Every check then uses one clock, synced to Amazon Time Sync Service, and the rule reads in the club's own time zone. Even if the club later runs two app instances, they can't disagree.
What it costs
| Line | Monthly, roughly |
|---|---|
EC2 t4g.small |
~$8 |
RDS db.t4g.micro, Single-AZ, with backups |
~$15 |
| Storage, 20 GB gp3 | ~$3 |
| Public IPv4 address | ~$4 |
| Total | ~$30, plus Meta's per-message WhatsApp fees |
At about 4,500 bookings a month, that's under 1 cent a booking.
The same design on GCP and Azure
| Role | AWS | Google Cloud | Azure |
|---|---|---|---|
| One small server | EC2 t4g.small |
Compute Engine e2-small |
A B-series VM |
| Managed PostgreSQL | RDS for PostgreSQL | Cloud SQL for PostgreSQL | Azure Database for PostgreSQL |
| End User Messaging Social | A WhatsApp Business provider | Azure Communication Services or a provider |
The shape doesn't change. On any cloud, the right answer for a weekly ten-second spike is one small machine and the smallest managed database.
Kavya changes her mind
After a Sunday where the instance died at 7:58pm and the release slipped by forty minutes, Kavya says it must never happen again. What's the cheapest change that delivers that, and what does it cost?
Show one answer
Look at what actually broke: the app instance, which holds no data. So protect that first. Put the instance in an Auto Scaling group with a size of exactly one, behind a load balancer. A dead instance is replaced automatically from its launch template, and the load balancer sends traffic to the new one without any DNS change. That adds about $17 a month for the load balancer, plus a few dollars of traffic.
The database is the harder part to bring back, and it's the next question to put to Kavya: a Multi-AZ database adds about $18 a month and turns a database failure into a minute or two of errors instead of a restore. Together the bill roughly doubles to $65 or so. Lay out both steps and let her choose. "Never" is a business decision, and now she knows its price.
What to remember
- Voice notes and one-line briefs hide most of the requirements. "People claim the same court" is a consistency requirement in disguise.
- Size the worst moment, not the average day. A weekly 10-second spike can set the design.
- Keep the hot path to one short transaction, and do slow work like notifications after the commit.
- Let the database enforce rules that can be raced: a unique index for "one per slot", a conditional update on a counter row for "at most N".
- Put related steps in one transaction so a failure undoes all of them.
- Some questions, like fairness, are product decisions with technical consequences. Lay out the trade-offs and let the owner choose.
- On a cloud, right-size from the requirements: a weekly ten-second spike and an owner who accepts a delayed release is one small instance and the smallest managed database, about $30 a month. Every extra box must buy something the brief asked for.
- Managed databases often beat a modest backup requirement for free (point-in-time restore to within minutes). Keep time rules in one clock, with an explicit time zone.