URL Shortening Service like TinyURL
Scalable system design for creating short URLs, redirecting users, and collecting analytics.
Command Query Responsibility Segregation — separate read and write models for optimized performance and scalability.
Rough, order-of-magnitude numbers. The point is to justify the architecture, not to be exact.
A diagram shows what exists; a trace shows what happens. Press play on a flow, or click any step, and the diagram above lights up the exact path that request takes.
From a validated command to a durable, committed state change.
How a read is served without ever touching the write database.
What happens when the read side needs a new shape, or recovers from corruption.
Every box on the canvas, and the job it does. Click a card to locate it in the diagram; click the node itself for the full deep dive.
Most systems read far more than they write — order status gets checked constantly, orders get placed comparatively rarely. CQRS (Command Query Responsibility Segregation) takes that asymmetry seriously by giving reads and writes two entirely separate models, each free to be optimized for its own workload instead of compromising on one shared schema.
A command mutates state and enforces business rules. A query only reads and is never allowed to change anything. Once that split is explicit, the two paths can scale, fail, and evolve completely independently.
The Command API is the only thing allowed to write. It validates every command against the domain's rules and commits the result to write-db inside a transaction — normalized, correct, and boring on purpose. Boring is the point: the write side's only job is to never let bad data through.
write-db never talks to read-db directly. Instead, a change feed carries every committed write to the projector (syncer), which transforms it into whatever shape the read side needs and upserts it into read-db.
This makes the read model:
The Query API adds one more optimization on top of the already-denormalized read-db: a cache-aside Redis layer. Because reads outnumber writes 50 to 1, absorbing repeat reads in Redis is often more impactful than anything that could be done to read-db itself.
CQRS trades strict consistency for independent scalability. A client that writes and then immediately reads its own write may briefly see stale data. That is an explicit, bounded cost — not a bug — and it buys a read path that can be cached, denormalized, and scaled without ever putting the write path's correctness at risk.
An architecture is a record of trade-offs. For every major choice here: what won, what lost, and why the constraints made it so.
A 50:1 read:write ratio means the two paths have almost nothing in common operationally. Forcing them to share one schema means every index that helps reads slows writes, and every write-optimized constraint forces a join at read time. Splitting the models lets each be tuned — and scaled — for what it actually does.
A distributed transaction across two different databases couples their availability — a slow read-db would now block every write. Streaming changes through a projector keeps write latency dependent on write-db alone, at the cost of read-db lagging by up to the ~300ms p99 budget. That trade is worth it because nothing here needs read-your-write guarantees at the database layer.
At 250K qps, hitting read-db for every request would require a database cluster sized for peak read traffic even though most of those reads repeat the same handful of hot orders. A cache-aside layer absorbs the repeat traffic cheaply, leaving read-db sized for genuine cache misses instead of total volume.
The write schema is shaped to make single-order updates safe and atomic, not to answer 'show me every order for this customer in the last 90 days' quickly. Denormalizing lets each read model pre-compute exactly the shape its query needs, so adding a new dashboard is a new projection, never a migration on write-db.
Reading a system means sensing where it cracks under 10× load. These are the pressure points of this design, and how it holds.
A customer places an order, then immediately checks its status and — because the projector hasn't caught up yet — doesn't see it. This is the defining failure mode of CQRS, not an edge case.
Bound and monitor replication lag as a first-class SLO (the ~300ms p99 target). For flows that truly need read-your-writes, route that specific read to write-db instead of read-db, or have the client optimistically render its own command's result.
A sustained spike in commands can outrun the projector's processing rate, growing the change-feed backlog and pushing replication lag well past its budget.
Scale the projector horizontally by partitioning the change feed, and alert on lag directly rather than inferring it — lag is the single metric that tells you the read side is falling behind reality.
read-cache only refreshes on a miss or explicit invalidation, so it can keep serving an order's old status well after read-db (and write-db) have already moved on.
Invalidate cache entries from the same change feed the projector consumes, and keep a short TTL as a backstop so a missed invalidation self-heals within seconds.
As new query shapes get added, each one is its own projection that can silently fall out of sync with write-side changes — a broken projector can corrupt a read shape for hours before anyone notices a wrong number on a dashboard.
Version each read projection, treat 'replay from scratch' (see the rebuild flow) as a routine operational tool rather than a last resort, and alert on projector error rate, not just lag.
If you can answer these without scrolling back up, the architecture is yours. Try each one out loud before revealing.
Every great system starts as a sketch on a whiteboard. The ability to zoom out and see the whole picture (how services connect, where data flows, what breaks and why) is what separates engineers who build features from engineers who build systems. This diagram is more than boxes and arrows. It's a map of decisions, trade-offs, and intentional design.
Studying architectures isn't just about passing interviews. It's about training your intuition. The more systems you take apart, the better you get at sensing where a monolith will crack, where a queue belongs, or when a cache is hiding a deeper problem. You start seeing patterns instead of chaos.
So keep reading, keep tracing those edges, keep asking "why this way and not that way." The engineers who truly understand large systems are the ones who never stop being curious about how things fit together. That curiosity is the only ingredient that really matters.
Atharva Arbat@arbat_atharva