Data & System Architecture, from the ground up Lesson 5 / 80

Trade-offs are everything

Latency vs throughput, consistency vs availability, simple vs flexible. The named trade-off catalogue and why 'we want it all' is the most expensive request in the room.

If you remember nothing else from this course, remember this lesson. It is the single sentence the rest of the 80 lessons orbit around.

Every architectural decision is a trade-off. There is no “best” architecture, only “best fit for this constraint set.” A system that is fast on small data is usually slow on big data. A system that scales horizontally to a thousand machines usually does so by giving up something else, often consistency, often operational simplicity. A system that is dead-simple to operate is usually one that doesn’t need to do very much yet. The minute you ask it to do more, the simplicity starts spending itself down.

When a stakeholder walks into a room and says “we want a system that is fast, scalable, consistent, cheap, easy to operate, and easy to extend,” what they are actually saying is that they don’t yet know which of those they care about most. Your job, as an architect, is to translate “we want it all” into “here is what we’ll be excellent at, here is what we’ll be merely adequate at, and here is what we will deliberately sacrifice.” That last list is the one nobody wants to write. Writing it is the job.

This lesson is the index of the named trade-offs that come up most often in real systems. We are not going deep on any of them yet. The rest of the course is, in large part, deep dives into individual trade-offs and how real systems navigate them. Today is the map.

The thesis

Every quality of a system has a cost in another quality. There is no free lunch in distributed systems, in databases, in storage formats, or in deployment models. The closest you will come to “we have it all” is “we are good enough on most of them, and bad on the few that turn out not to matter.” Recognising in advance which qualities don’t matter, for this system, with this user base, on this budget, is most of architecture.

The phrase “good enough on most, bad on a few” is not defeatist. It is the structural truth of building anything that exists in the real world. A car that goes 300 km/h is not also a fuel-efficient family hatchback. A database that scales linearly to 1,000 nodes is not also the easiest thing to debug at 3 a.m. A monolithic Rails app that you can deploy in 90 seconds is not also the substrate you will run a 200-engineer organisation on. Each of these is a fine product. Each of them is fine because somebody decided what mattered.

The named trade-offs

What follows is the catalogue. Not exhaustive: I have left out the small ones. These are the seven that come up in almost every real architecture conversation I have been in.

1. Latency vs throughput

Latency is how long one request takes. Throughput is how many requests the system handles per second. They are not the same thing, and optimising one often hurts the other.

A system batched into groups of 1,000 requests, processed every 100 ms, has excellent throughput (10,000 requests/sec) and terrible latency (100 ms minimum, often more). A system that processes each request as it arrives has excellent latency (sub-millisecond, sometimes) and worse throughput, because batching amortises overhead and unbatched work doesn’t get to.

In an analytics warehouse you usually optimise throughput; the user who runs a GROUP BY country over 4 TB does not care whether it takes 8 seconds or 12, but they do care that the cluster can serve 50 such queries concurrently. In a payment-authorisation system, latency wins; the merchant terminal needs an answer in under 200 ms, every time, even if it means the throughput per node is a fraction of what it could be.

The trap is treating “fast” as a single quality. It isn’t. Whenever someone says they want a system that is “fast,” your follow-up question is: fast at what, for whom, under what load? The answer is rarely both axes.

2. Consistency vs availability

This is the CAP theorem trade-off, and it gets a full lesson of its own (lesson 10), but the headline is: in a distributed system, when the network partitions, you have to choose. You can refuse to serve requests until the partition heals (consistency, at the cost of availability) or you can serve potentially stale data on both sides until they reconcile (availability, at the cost of consistency).

Your bank’s account-balance service picks consistency. It would rather show you an error page during a network partition than tell you that you have €100 in an account that has actually been overdrawn for ten minutes. Your social network’s like-counter picks availability. It would rather show you a slightly-stale number, or even a number that briefly goes down before going up, than refuse to load the page entirely.

Neither answer is “right.” They are different products with different tolerances for being wrong. The DDB-vs-Spanner-vs-Postgres conversation is, at its heart, this trade-off in three different shapes. We will spend a lot of time here.

3. Read-optimized vs write-optimized

The shape of your storage engine determines what it is fast at. B-tree-based engines (Postgres, MySQL with InnoDB, SQL Server) are excellent at point reads and range scans on indexed columns; their write path is more expensive because every insert may need to rebalance pages on disk. LSM-tree-based engines (RocksDB, Cassandra, BigTable, ScyllaDB, parts of Mongo) are excellent at high-volume writes; their read path is more expensive because a key may live in any of several layered SSTables and the engine has to merge results.

If you are building a system that mostly reads (a CMS, an internal dashboard, a typical web app), pick a B-tree-shaped database. If you are building a system that mostly writes (a metrics backend, an event log, a chat history store), pick an LSM-shaped database. If you are building a system that does both, pick the one whose weak side you can compensate for with caching, batching, or a separate read store.

This trade-off has been doing real work on system design for thirty years and shows no sign of going away. We come back to it in module 4 (data systems).

4. Simple vs flexible

A YAML config file with two settings is simpler than a plugin system. A plugin system is more flexible. Both have their moment. Picking the wrong one for the moment you are in is one of the most common architectural mistakes.

Early in a project’s life, simple wins. The two cases you have today are the two cases you have today, and a config that handles them in 30 lines is faster to write, faster to read, faster to debug, and faster to delete when the requirements change. The flexibility you would have built into a plugin system is paying interest on a loan you never took out.

Later, when you have 50 cases instead of two, simple loses. The 30-line config has become a 3,000-line config; every entry is a special case; nobody can predict what change will break what. Then the plugin system you didn’t build five years ago starts looking like a bargain.

The skill is in noticing which side of the inflection point you are on. The honest default is: build simple, watch it grow, refactor to flexible when the cases stop being addable cleanly, not when they are merely numerous. “Premature flexibility” has cost more projects than “missing flexibility.”

5. Decentralized vs coordinated

Scale wants decentralization. Correctness wants coordination. These two pull in opposite directions on almost every distributed-systems question.

A globally-distributed cache that lets each node decide what to evict is decentralized; it scales linearly and survives any single node dying, but two nodes can hold inconsistent state for a while. A cache that uses a central coordinator to assign keys to nodes is coordinated; the state is always consistent but the coordinator is now a bottleneck and a single point of failure.

DNS is decentralized. It scales to the entire planet because no node has to coordinate with any other. The cost is that propagation takes minutes to hours; you cannot use DNS as a real-time consistency mechanism. A traditional relational database is coordinated. Every write goes through one primary, which is why ACID is straightforward and why scaling writes past one machine is hard.

Most real systems pick a midpoint: coordinated within a small blast radius (a service, a region, a partition) and decentralized between blast radii. Knowing which axis is worth which coordination cost is half of system design.

6. Cost of build vs cost of buy

Many architectural decisions are dressed up as technical questions but are actually pricing questions. “Should we use Kafka or Kinesis?” is rarely about the engineering merits of the two systems. It is about whether you would rather pay AWS roughly $0.08 per GB for a managed product with no operations work, or pay your own engineers to run a Kafka cluster that costs less per GB but eats two engineers’ on-call rotation.

The same question recurs everywhere: managed Postgres vs self-hosted, Auth0 vs your own auth service, Stripe vs your own payment processor, Snowflake vs a self-hosted Spark+Iceberg lakehouse. The answer almost always changes with team size and growth stage. A two-person startup buying everything from vendors and shipping product is making the right call. A 200-engineer company paying $4M/year for Auth0 has probably crossed the line where building it would have been cheaper. A 2,000-engineer company that builds its own everything is back on the wrong side.

The trap is that the answer is not stable. The right call at 5 engineers is wrong at 50, and the right call at 50 is wrong at 500. Architecture has to revisit cost-of-build vs cost-of-buy on every major piece, every couple of years. Most teams don’t, and end up paying eight figures per year for something they could now run themselves, or sinking a team into maintaining a homegrown system that is now strictly worse than the SaaS they could buy.

7. Time to market vs long-term cost

The last trade-off, and arguably the meta-trade-off that contains all the others. Every shortcut you take today has a future cost. Every investment you make in long-term clean architecture has a today cost.

If you are at a startup with 18 months of runway, the right call is almost always to ship the duct-tape version. Hardcoded values, no tests on the boring paths, no documentation, no abstractions, all features behind feature flags so you can rip them out cheap. You are racing the runway, and most of the duct tape will be replaced anyway because the product you ship is almost certainly not the product the market wants. Investing in clean architecture for a feature that gets cut in three months is the most expensive form of work.

If you are at a 10-year-old company with strong revenue, the right call inverts. The duct-tape you write today will still be there in 2034, will be operated by people who haven’t joined the company yet, and will accumulate compound interest in maintenance cost. Spending an extra week to do it cleanly is paying down a 40-week debt that someone else would have inherited.

Most engineers’ instincts are calibrated to one end of this trade-off, based on where they spent their formative years. The skill is in noticing which environment you are in now and adjusting accordingly. A startup engineer who writes Google-scale architecture in week one ships nothing. An enterprise engineer who duct-tapes the credit-card flow ships a regulatory incident.

The 2x2

Here is one way to picture four of these trade-offs as named quadrants:

flowchart TD
    subgraph Q1["High consistency, high availability"]
        A1["Hard. Spanner, FoundationDB.<br/>Achievable with cost and complexity."]
    end
    subgraph Q2["High consistency, low availability"]
        A2["Traditional RDBMS in single primary.<br/>Postgres, classic SQL Server."]
    end
    subgraph Q3["Low consistency, high availability"]
        A3["Eventual consistency.<br/>DynamoDB default, Cassandra, DNS."]
    end
    subgraph Q4["Low consistency, low availability"]
        A4["Nobody chooses this on purpose.<br/>It's where misconfigured systems land."]
    end

Three of those four quadrants are real architectural choices for real workloads. The fourth, low-consistency-low-availability, is the place systems end up when nobody made a deliberate choice and the defaults all pointed in the wrong direction. If your post-mortem reads “we lost data and the service was down too,” you are in quadrant four, and quadrant four is always an accident.

”We want it all”

The most expensive sentence in the architecture meeting is “we want it all.” Fast, scalable, consistent, cheap, simple, flexible, and ready by Q2. Real systems do not exist on this point of the design space. They cannot. The closest the industry has come is Google Spanner, which gets you geo-distributed strong consistency at the cost of being expensive to run, expensive to license, requiring atomic clocks (yes, real ones), and being subject to the latency floor of synchronous WAN replication. Spanner is roughly the most expensive system in the industry, and it is the one that makes the fewest trade-offs. That is not a coincidence; it is the trade-off structure.

When somebody at the table asks for all of it, your job is not to argue with them. Your job is to translate. “If we optimise for latency at p99 below 50 ms, we will pay for it in throughput per machine and in regional availability; here are three architectures and what each gives up.” That sentence costs them nothing if the priorities are still flexible, and it costs them weeks of their delivery date if they are not. Either way you have informed the decision instead of being surprised by it later.

The architects who burn out are the ones who try to deliver “all of it” and find six months later that the system is mediocre on every axis because the trade-offs were never made consciously. The architects who don’t burn out are the ones who make the trade-offs early, write them in an ADR (lesson 4), and revisit them every six months. The mediocre system is the unavoidable cost of refusing to choose. The merely-adequate-on-most-things-and-excellent-on-the-things-that-matter system is the one that wins.

What’s coming

The rest of this course is mostly named trade-offs and how real systems navigate them. Lesson 6 walks through the simplest possible architecture (one VM, one database) and shows what it gives up to be that simple. Lessons 9 through 12 go deep on consistency vs availability and the CAP family. Module 4 is the read-vs-write storage engines fight. Module 6 is the buy-vs-build, simple-vs-flexible balance for queues, caches, search, and ML serving. Module 9 is the time-to-market vs long-term-cost story for the operational side: deployments, observability, on-call.

Every one of those lessons starts from the same foundation: this thing is a trade-off, here are the axes, here is which one this technology picks, here is what it gives up, and here is when you’d pick the other one.

Architecture is not a set of patterns to memorise. It is a set of trade-offs to navigate. The patterns are second-order; they are crystallised solutions to specific trade-off configurations that come up often enough to deserve a name. We will get to plenty of them. We will arrive at every one of them by way of the trade-off it resolves.

Next lesson: the simplest architecture you can ship. One VM, one Postgres, one process, and an entirely respectable career built on top of that shape.

Search