Published Nov 18, 2024

Two Principles That Survive Every System Design

Layering and loose coupling aren't trendy patterns — they're the two properties that determine whether a system stays changeable as it grows.

Dark teal abstract cover

Most systems start simple. A single file, a few functions, a database connection somewhere. Then they grow, and growth reveals whether the structure underneath is load-bearing.

Two principles have held up for me across every non-trivial system I’ve worked on: layering and loose coupling. They’re not new ideas. But they’re worth understanding precisely, because the wrong version of each is almost as bad as neither.

Layering

A layered system separates concerns into distinct tiers, where each layer only knows about the layer directly below it. The classic form is: interface on top, logic in the middle, storage at the bottom.

Layered architecture showing UI, business logic, data access, and storage stacked vertically

The rule isn’t the picture — it’s the direction of knowledge. The UI knows about the logic. The logic knows about data access. Data access knows about storage. Nothing flows upward. The UI has no idea what database you’re using. Storage has no idea what a user is.

This matters for two reasons.

First, it limits blast radius. When the database changes, only the data access layer needs to update. When the UI framework changes, only the top layer is touched. Changes stay local.

Second, it defines what’s testable. If your logic layer reaches directly into the database, you can’t test it without a database. If it only depends on an interface it receives, you can test it with anything that implements that interface — including a simple in-memory stub.

The failure mode is layer skipping: the UI making direct database calls, or the business logic importing constants from a config file three layers down. Once a shortcut crosses a layer boundary, the layers stop being real. You have the diagram but not the property.

Loose Coupling

Coupling is about how much one component needs to know about another in order to use it. Tight coupling means one component is deeply familiar with the internals of another. Loose coupling means components speak through narrow, stable contracts.

Diagram contrasting tight coupling with many crossing arrows versus loose coupling through a central interface

The diagram makes it visual: tightly coupled components draw lines to each other’s internals. Replace any one module and you break everything connected to it. In a loosely coupled system, each module connects to a shared interface — you can swap out the implementation without touching anything else.

In practice, loose coupling often looks like:

  • Passing a logger as an argument rather than importing a global one
  • Accepting a Storage interface rather than a PostgresClient directly
  • Publishing events to a bus rather than calling another service’s functions directly

The key is that the caller doesn’t need to know who is on the other side of the interface, only what it can do.

The failure mode here is interface inflation: creating a thin interface wrapper around a specific implementation, and then letting callers leak the implementation anyway. An IDatabase that has runPostgresQuery() on it is still tightly coupled to Postgres — the interface is cosmetic.

The Relationship Between Them

Layering and loose coupling reinforce each other. Layers give you a map of where coupling is allowed to flow. Loose coupling makes each layer’s boundary stable and explicit.

A system can have layers without loose coupling — the layers are real, but each one reaches into the internals of the next. It’s better than no layers, but fragile. A system can have loose coupling without layers — components pass interfaces to each other freely, but with no directional rules, you end up with cycles.

The combination is what makes a system that can be changed confidently: you know which direction dependencies flow, and you know that each boundary is defined by a contract rather than an implementation.

What This Doesn’t Mean

Neither principle means you should prematurely abstract everything. A system with two endpoints and one table probably doesn’t need a formal repository layer. The principles become load-bearing as systems grow and as multiple people work on them simultaneously.

The question to ask isn’t should I apply these patterns — it’s where are the likely pressure points. That’s usually wherever: multiple people will edit the same area, or an external dependency (a database, a third-party API, a queue) could change.

Start flat. Add structure where the system shows you it needs it.