Skip to content
Design principles

Software design principles: coupling, cohesion, and change 🧭

The core problem in software development is not writing code. With enough caffeine and stubbornness, that usually happens. The real problem is handling change without turning every modification into a minor diplomatic incident between modules, teams, and infrastructure.

That is why design principles matter more than fashionable buzzwords: they exist to reduce the cost of change, not to make the diagram look smarter.

Coupling: when everything depends on everything 🔗

Coupling measures how much one part of the system depends on another. The higher the coupling, the more a local change tends to spread in unpleasant ways.

Typical signs:

  • changing one module forces you to touch three more
  • tests need half the application to boot
  • business logic knows database, framework, or network details

Some coupling is inevitable. The point is not to eliminate it, but to make it intentional, explicit, and stable.

Cohesion: belonging together for a good reason 🧲

Cohesion measures how strongly the elements of a module belong to the same concern. A cohesive component has responsibilities that naturally fit together. A low-cohesion component is the software equivalent of a junk drawer.

Signs of low cohesion:

  • one function validates data, calls an API, writes to the DB, and formats the output
  • one class contains business rules, logging, retries, and configuration
  • the module name is so generic that it says almost nothing: Utils, Manager, Helper

Less coupling and more cohesion are not polite slogans. They are how you get less traumatic refactoring.

Separation of concerns: let each part do its job 🧹

Separation of concerns prevents the domain from becoming a landfill of technical details. It means separating things that change for different reasons.

Simple example:

  • the domain decides what is valid
  • infrastructure decides how data is stored or sent
  • the interface decides how state is presented to a user

When everything ends up in the same place, the system becomes brittle. When boundaries are clear, changing technology or behavior costs less.

Encapsulation and information hiding: protecting invariants 🛡️

Encapsulation is not just hiding fields behind methods with respectable names. It means protecting the invariants of the system.

Information hiding adds a stronger point: other modules should not need to know how something works internally, only which contract it offers.

If a caller must understand internal details to use a component correctly, then the component is not hiding much. It is just shy.

Composition over inheritance: fewer family trees, more collaboration 🧩

Inheritance promises reuse, but often delivers rigid hierarchies and debugging sessions with an archaeological feel. Composition usually works better because it builds behavior by combining small, replaceable pieces.

Use composition when:

  • you want to vary behavior without rebuilding a hierarchy
  • you need to swap collaborators in tests or at runtime
  • the relationship between components is about collaboration more than identity

Inheritance is not forbidden. It is just often more expensive than it looks at first.

The cost of change is the real metric 💸

A sensible design is not the most abstract one, nor the cleanest in an aesthetic sense. It is the one that makes likely changes cheaper.

Useful questions:

  • which part of the system will change most often?
  • which dependencies are volatile and which are stable?
  • where is an abstraction actually worth paying for?
  • what am I protecting: the domain, the framework, or my ego?

If an abstraction lowers the cost of real changes, good. If it exists only to satisfy a theory, you are probably prepaying a debt nobody asked for yet.

Real trade-offs: simplicity versus rigidity ⚖️

Every design choice is a trade-off:

  • too much simplicity can hide dangerous coupling
  • too much abstraction can become over-engineering
  • too many interfaces can hide the actual logic
  • too little isolation can make everything fragile

The right question is not “which principle is universally correct?” but “which structure makes this system more adaptable without crushing it under its own weight?”

Practical review checklist ✅

  • does this module have a readable responsibility or is it a generic container?
  • are dependencies explicit or hidden behind global state and magic?
  • am I separating concepts that change for different reasons?
  • does the caller know internal details it should not care about?
  • does a local change remain local, or does it spread too far?

In short 🧾

Coupling, cohesion, separation of concerns, encapsulation, and composition are not academic decorations. They are tools for building software that can tolerate change without reacting like a house of cards in a windy room.

If you want to get more concrete from here, the natural next steps are S.O.L.I.D., Clean code, and the guide on Dependency Injection and Inversion of Control.

Last updated on