system-design.md
System design
Boundaries, data flow, failure modes
System design
The review that keeps getting pushed past launch
Most systems get hard to maintain because the important decisions were made in passing. A feature has to ship. A service needs another team’s database. A third-party API has to be in before a customer demo. A background job behaves under normal load, so nobody asks what happens when it runs twice.
Each of those moves the project forward. They also set boundaries, dependencies, and operational behavior that get expensive to change later.
I help engineering teams make those decisions on purpose. I work through the architecture before the implementation hardens around assumptions nobody has examined. I define what each service is responsible for, who owns the data, how information moves, and what happens when a dependency fails.
I sit with the engineers who will build and operate the system, challenge the design in the room, and turn the idea into a plan that can survive production. The tradeoffs should be visible, the approach should fit the requirements you actually have, and the next engineer should be able to read why the system works the way it does.
The engagement usually starts because “we’ll fix that in v2” has been said twice in a month, because architecture discussions end without a decision, or because the team can explain how the system works and still cannot say what happens when part of it fails.
A diagram is not the design
A diagram can show an API talking to a service, a database, and a queue. It usually does not say why those pieces exist, which one owns a given fact, or what happens when the network between them gets unreliable. It does not say whether a request can be retried, whether two services may update the same record, or whether a failed background job leaves anything recoverable.
Those details are how the system behaves once several users act at once, an external dependency slows down, or a deploy is incompatible with what is already running. I put those conditions in the conversation before they show up as incidents.
The organization building the system is a constraint. A design that assumes dedicated infrastructure, security, and platform teams is the wrong design for three engineers. I want an architecture that meets the requirements, states its tradeoffs, and can be built and kept by the people who will own it.
What I bring
Requirements that actually shape the design
Architecture conversations often open with a solution. “We should use microservices.” “We need Kafka.” “This should be event-driven.” “We can put everything in Kubernetes.” Any of those can be a reasonable choice. None of them is a requirement.
I start from the problem the system is meant to solve, the people and processes it serves, and the conditions it has to run under. I separate what it must do from the constraints around it: how many users or transactions, what latency is acceptable, how much downtime the business can tolerate, what information is sensitive, and which external systems it has to talk to. I also look at the constraints that get skipped: deadlines, the infrastructure you already have, the expertise in the room, compliance, migration, and what it costs to operate after launch.
Where a requirement is still a guess, I write the uncertainty down instead of promoting it to a fact. You get criteria you can use to judge a design. If a component adds complexity and serves no real requirement, it has to justify itself.
Boundaries and ownership
A lot of architectural trouble starts when nobody owns the state. Two services both think they own the same data. An application reads another application’s database directly. A shared library collects business logic nobody wants to touch. A service that was supposed to be independent cannot ship unless several other systems change with it.
Another service rarely fixes that, so I help the team name which capabilities belong together, which responsibilities stay separate, and which interfaces they use. I look at domain boundaries, service contracts, database ownership, shared dependencies, and how the components relate. When several systems need the same information, I say which one is authoritative and how a change gets communicated.
I also separate a logical boundary from a deployment boundary. A modular monolith can give you the separation you need without the operational cost of distributed services. Separate services make sense when the workloads differ in scale, ownership, reliability, or release cycle. I am after explicit ownership and dependencies you can manage.
Data flow, before anyone writes the implementation
Where data lives, how it moves, and whether it stays correct matters more than the language or the framework. I work through where information starts, how it is validated, where it is stored, which components may change it, and how downstream systems hear about the change. That covers synchronous requests, asynchronous work, database transactions, events, caching, replication, and systems outside yours.
The dangerous spots are where data crosses a boundary. A transaction commits and the event never publishes. A consumer handles the same message twice. Two updates conflict. The design has to be honest about what it can promise on ordering, consistency, and delivery.
Depending on the requirements, that might be a transactional outbox, idempotent consumers, change data capture, an event-driven workflow, or a plain request and response. I pick from the problem, not from whichever pattern is popular this year.