Skip to content

Key Concepts

The vocabulary I reach for when reasoning about a design, independent of any specific pattern or technology.

  • Separation of concerns — dividing a system by distinct responsibility. Each part addresses one concern, so a change to one doesn’t force changes to the others. Concerns can be technical (presentation, persistence) or business capabilities (orders, billing), and splitting by business capability usually keeps a feature change in one place.
  • Modularity — decomposing a system into independent units. Modules can be understood, built, and replaced separately, but the value depends entirely on where the boundaries are drawn.
  • Coupling — degree of interdependency between components. The practical test is how far a change ripples: tightly coupled components have to change, be tested, and be deployed together.
  • Cohesion — how focused a component’s responsibilities are. High cohesion means everything in a module belongs together, which usually goes hand in hand with low coupling.
  • Abstraction — hiding implementation detail behind interfaces. A good abstraction lets callers ignore the detail; a leaky or premature one just adds indirection without removing complexity.
  • Encapsulation — bundling data and behaviour, controlling access. It protects invariants by making sure state can only change through operations that keep it valid.

Taken together, these describe one line of reasoning rather than six separate rules: separation of concerns decides where to split a system, coupling and cohesion show whether the split worked, and abstraction and encapsulation keep it working as the system changes.

What matters most to me is the balance between coupling and cohesion. They work together, and getting that balance right is one of the main tools for managing complexity. That balance also has to account for logical versus physical coupling, which leads straight into the question of microservices versus a modular monolith. Loosely coupled modules at the logical level manage complexity without the extra cost of decoupling them at runtime.

Two sets of three modules. On the left, each module mixes unrelated elements and many links cross between them: tightly coupled, low cohesion. On the right, each module holds only related elements, with just two links between modules: loosely coupled, high cohesion.

Abstraction matters just as much, and it overlaps with ideas such as Domain-Driven Design and Screaming Architecture. But it can be overdone: premature abstraction, or too many layers of it, makes code much harder to understand. This is where DDD’s ubiquitous language helps, because abstractions should ideally match the problem domain rather than the technical structure.

  • Scalability — a system’s ability to handle increased load by adding resources. It can be vertical (bigger machines) or horizontal (more machines); horizontal scaling usually requires stateless components or partitioned data.
  • Reliability — a system consistently performs its intended function correctly over time. It’s about producing correct results, not just being up — a system can be available but unreliable if it returns wrong answers.
  • Availability — the proportion of time a system is operational and able to serve requests. Usually expressed in “nines” (99.9% allows roughly 8.8 hours of downtime a year; 99.99% about 53 minutes) and achieved mainly through redundancy and failover.
  • Resilience — a system’s capacity to absorb failures and recover to normal operation. Techniques like retries, timeouts, circuit breakers, and bulkheads contain a failure so it doesn’t cascade through the system.
  • Maintainability — how easily a system can be modified, fixed, or extended. It’s shaped mostly by the structural concepts above, and it dominates long-term cost because most of a system’s life is spent being changed.
  • Testability — how easily a system’s correctness can be verified through testing. It improves when core logic can be tested in isolation, with no dependency on infrastructure concerns, and when modules are cohesive and focused.
  • Observability — how well a system’s internal state can be understood from its external outputs (logs, metrics, traces). Monitoring tells you something is wrong; observability lets you work out why, including for failures nobody anticipated.
  • Performance — how efficiently a system uses resources to complete work (speed, resource consumption). It should be defined against measurable targets (e.g. p95 latency) rather than a general sense of “fast”.
  • Security — protecting a system and its data from unauthorised access, misuse, or harm. It covers confidentiality, integrity, and availability, and is far cheaper to design in than to retrofit.
  • Portability — how easily a system can be moved or adapted to a different environment. It’s a trade-off: avoiding platform-specific features preserves options but can mean giving up capabilities you’d otherwise get for free.

These attributes compete with each other, so no system can maximise all of them. Tuning for performance often costs maintainability, portability can mean giving up platform features that would help performance, and adding redundancy for availability or instrumentation for observability adds complexity and overhead. So a large part of the architect’s job is prioritisation: working out which few attributes matter most for this system and its users, and knowingly accepting weaker results on the rest. Being explicit about those priorities early is what lets trade-offs later be settled by reasoning rather than by preference.

My own priorities shift over a project’s lifecycle. At the start, security, maintainability and testability come first. Security in particular is a given: it has to be designed in from day one, not added later. I also put observability in place early, because it is what unlocks the other attributes later on. Reliability matters from the outset too, since a system that returns wrong answers isn’t doing its job at any stage.

As a project moves from early development to pilot, then launch and ongoing support, the other attributes rise in priority. That shift should be driven by the NFRs and by what observability shows, not by guesswork. Performance is the clearest example: I’d rather measure it through monitoring in early production than optimise prematurely up front.

Portability is usually the lowest priority. If you’re never going to change the data store, there’s little point abstracting it so it can be swapped out, which is the repository debate that comes up regularly in the .NET community. That debate sometimes misses the mark, though. A repository often earns its place even when the database will never change, because it keeps data access in one place and separates the core business logic from infrastructure concerns. Testability isn’t the reason for me: I wouldn’t mock out a repository, and I’m not keen on swapping in an in-memory replacement either.

  • State — where data lives, how it’s shared across components, and how it changes over time. Stateless components are easy to scale and replace, so the design effort goes into deciding where state lives and who owns it.
  • Consistency — whether all parts of a system see the same data at the same time (strong consistency), or are allowed to temporarily diverge and converge later (eventual consistency). Strong consistency simplifies reasoning but costs latency and availability; eventual consistency scales better but pushes complexity onto the application and its users.
  • Partition tolerance — the system continues to function despite network failures that prevent some components from communicating with others; under the CAP theorem, a partition forces a trade-off between consistency and availability. Partitions aren’t optional in a real network, so the actual design choice is how the system behaves when one happens.
  • Latency vs. throughput — latency is the time to complete a single operation; throughput is the volume of operations a system can handle over time — the two often trade off against each other. Batching, for example, raises throughput at the cost of per-request latency, so it matters which one users actually feel.
  • Concurrency and parallelism — concurrency is multiple tasks making progress during overlapping time periods; parallelism is multiple tasks executing simultaneously on separate resources. Concurrency is about structuring work so it can interleave; parallelism is about hardware executing it at once — you can have either without the other.
  • Synchronous vs. asynchronous communication — synchronous calls block until a response returns; asynchronous calls continue without waiting, often via callbacks, events, or messages. Synchronous is simpler to reason about but couples callers to the availability and latency of what they call; asynchronous decouples them at the cost of harder debugging and eventual consistency.
  • Idempotency — an operation produces the same result no matter how many times it’s performed, which matters for safe retries. Most messaging systems guarantee at-least-once rather than exactly-once delivery, and a caller that times out can’t tell whether the operation ran, so operations that may be retried usually need idempotency keys or naturally idempotent designs.

Most of these concepts come from one fact: the network between components is slow, unreliable, and can fail partway through an operation. That’s the heart of L Peter Deutsch’s fallacies of distributed computing, and each concept above is a way of deciding what the system should do when the network doesn’t behave.

Synchronous versus asynchronous communication is a trade-off that’s often handled dogmatically. A rule that all communication between services must be event-based and asynchronous adds complexity that often isn’t needed. Rather than defaulting either way, I ask three questions:

  1. Is the trigger a business event? If it’s something that makes sense in the context of the overall business process, it should probably be published as an event. That reduces coupling between modules and makes the system easier to adapt when the business process changes.
  2. Is the processing long-running or resource-intensive? If the work can be offloaded and the caller doesn’t need the result straight away, a simple asynchronous process can improve throughput.
  3. Is availability between the services a concern? If one service shouldn’t fail just because another is down, asynchronous communication can improve availability and fault tolerance. The question is whether that gain justifies the extra complexity.

Each of these is a value judgement rather than a rule. In my experience, using an event-based, asynchronous model for everything adds complexity before it’s needed, in the same way premature abstraction does.