Article 2 of 8

Core Mental Models Every System Designer Needs

The five mental models that separate great system designers from average ones.

12 minBeginner
Key Takeaway

Without mental models, system design is guided guesswork. Five models separate engineers who build systems that survive production from those who build systems that look great on whiteboards: State vs Behaviour, Synchronous vs Asynchronous Flow, the Read/Write Split, Bounded Context, and Failure as a First-Class Citizen. Internalise these, and you'll have a principled framework for nearly every architectural decision you'll face — regardless of stack, scale, or domain.


There's a reason experienced engineers can walk into an unfamiliar codebase and quickly identify its structural problems. I've seen it happen — a senior architect spends 20 minutes reviewing a system and pinpoints the exact issue that three teams have been unable to find for weeks. It looks like intuition. It isn't.

It's pattern recognition built on mental models.

A mental model is a simplified representation of how something works. When you have the right mental models for system design, you stop reasoning from scratch every time. You have a framework that guides your decisions before you've fully mapped the problem.

That's the real difference between a 5-year engineer and a 15-year engineer reviewing the same system. Not more knowledge — better models.

Here are the five that matter most.


1. State vs Behaviour

Every system has two dimensions: the state it holds and the behaviour it executes.

State is what the system remembers: a database row, a cached value, a Kafka offset, a session token. Behaviour is what the system does: validates input, transforms data, sends a notification, processes a payment.

The most common architectural mistake I see — and I've seen it in every organisation I've worked with — is mixing state and behaviour without thinking. When they're tangled together, three things happen: the system becomes hard to scale (state is hard to distribute), hard to reason about (what happens when two processes modify the same state?), and hard to test (you can't isolate behaviour from its data).

The clearest example: a service that owns a database, caches data in memory, and process business logic all in the same component. It seems simple until you need to scale the processing layer independently, at which point you discover the state is stuck to the behaviour.

The question to ask always: Where does state live? Who owns it? What's the one source of truth for this piece of data?


2. Synchronous vs Asynchronous Flow

Synchronous flow means the caller waits for a response before proceeding. Asynchronous flow means the caller continues immediately and the result is processed separately, later.

Neither is universally better. The decision depends on one question: does the caller actually need the result before it can continue?

I've watched engineering teams default to synchronous calls because they're simpler to reason about, and only discover the cost when their checkout API starts timing out because it's serially waiting for a slow email-sending service. The checkout process needs to confirm the order — it does not need to wait for the welcome email to be queued.

The practical rule: If the caller doesn't need the result immediately, or if the operation takes more than ~200ms, consider making it asynchronous. You're not just improving performance — you're improving resilience. An async system can absorb downstream failures without cascading them upstream.

The flip side: async introduces complexity around ordering, failure handling, and observability. Don't reach for it reflexively. Apply it when the caller genuinely doesn't need to block.


3. The Read/Write Split

Almost every system handles more reads than writes. But by how much, and what does that ratio imply?

A social feed is read millions of times per day and written to thousands of times. A product catalogue is read constantly and updated occasionally. A logging system is write-heavy by definition. A financial ledger has aggressive read requirements during reporting periods and modest write requirements day-to-day.

Once you internalise this mental model, you start noticing opportunities everywhere:

  • Predominantly read-heavy? Think caching layers, CDN, database read replicas.
  • Write-heavy? Think write-optimised data structures, append-only logs, event queues, batch processing.

The ratio also tells you where your bottlenecks will be. A 100:1 read-to-write system doesn't need to optimise its write path — it needs to optimise the 100 times more common read path.

This is why patterns like CQRS (Command Query Responsibility Segregation) exist: when your read and write requirements diverge enough, using the same data model for both is a constraint you didn't have to accept.

The discipline: before you design any data flow, establish the read-to-write ratio. That single number drives more architectural decisions than almost anything else you'll identify in requirements.


4. Bounded Context

Borrowed from Domain-Driven Design, but practically essential regardless of whether you use DDD: a bounded context is the explicit boundary within which a data model is defined and applicable.

In plain terms: your "User" in the billing system does not need to be the same "User" as in your notification system. They have different concerns, different lifecycles, different fields that actually matter, and different ownership. Forcing them to share a model creates invisible coupling.

The classic failure mode I've repeatedly observed: a single users table that every service reads from. Works fine until someone adds a billing-specific column and breaks the notification service's schema expectations. Or the identity team migrates to a new primary key format and suddenly every service that joined against that table needs to be updated in lockstep.

Bounded contexts are the reason microservices can deliver on their promise. Each service owns its own data model. They communicate through events or APIs — never through shared database tables. When each team owns its own context, they can evolve it without coordinating with everyone else.

If you take nothing else from DDD, take this: shared tables are shared technical debt. Draw the boundary, own the model, communicate through contracts.


5. Failure as a First-Class Citizen

This is the mental model that, in my experience, most consistently separates senior engineers from mid-level ones.

The question isn't "will this fail?" — everything eventually fails. The question is: "what happens when it does, and is that acceptable?"

Every network call will fail at some point. Every database will occasionally be slow. Every third-party API will have downtime at a time that's inconvenient for your on-call engineer.

When you design with failure as a first-class consideration, you naturally start asking:

  • What do we do when this service goes down? (Circuit breaker, graceful degradation?)
  • What happens if this message is delivered twice? (Idempotency)
  • What if the retry storms hit a recovering dependency simultaneously? (Exponential backoff with jitter)
  • What if this queue backs up for 10 minutes? (Dead letter queues, consumer lag monitoring)
  • What's the blast radius if this component fails? (Bulkhead isolation)

These aren't theoretical edge cases. In a system that runs for years, every one of these scenarios will happen. The engineers who designed for them sleep through the incident. The engineers who assumed they'd never happen are the ones getting paged.


How These Models Work Together

These five models are not independent frameworks — they reinforce each other and show up together constantly.

Consider a notification service. It receives user events asynchronously (not every notification sender needs to wait for delivery confirmation). User preferences are state managed in a separate bounded context from the notification logic. That preference store is read far more than it's written, so we apply the read/write split: a cache sits in front of the preference database. When the email provider has an outage, we fail gracefully: circuit breakers detect the failure, the system queues messages for later, and the main application keeps serving requests.

That's not a series of independent decisions. It's five mental models applied coherently to one problem.

And that's what good system design actually looks like. Not boxes on a whiteboard — a coherent set of decisions guided by mental models that have earned their place through production experience.


Key Takeaways

  • Mental models are the real skill. They let experienced engineers reason about unfamiliar systems without starting from scratch every time.
  • State vs Behaviour: Always ask who owns state, where the source of truth is, and whether state and behaviour need to be co-located or can be separated.
  • Sync vs Async: Default to async when the caller doesn't need the result immediately, or when the operation is slow enough to become a latency bottleneck in the critical path.
  • Read/Write Split: Establish the ratio early. Different access patterns require completely different optimisation strategies — and treating reads and writes the same is often a performance mistake waiting to happen.
  • Bounded Context: Services should own their own data models. Shared tables create invisible coupling that surfaces as incidents six months after the decision was made.
  • Failure as a First-Class Citizen: Design for failure as a normal operational condition. Every call will eventually fail. Every queue will eventually back up. Every dependency will eventually be slow. Plan for all of it.