Skip to content

Design by Structural Principles

Chapter Info

Calculating... Writing Progress: 80%

!!Principles only matter once they take shape in the code.!! This chapter turns the mindset of good design into concrete structure: what to separate, who owns each responsibility, how to add behavior without breaking what works, and what should depend on what. Four classic principles — Separation of Concerns, Single Responsibility, Open-Closed, and Dependency Inversion — answer these questions and keep change local as the system grows.

alt text

Introduction [Introduction] {1}

From Principles to Structure [From Principles] {1}

The principles in Design by Principles guide how we think about design and make decisions. But those ideas eventually have to take shape in the software itself — in how responsibilities are divided, where boundaries are drawn, how components depend on each other, and how the system grows. !!Good design becomes concrete through the structure of the software.!! The following patterns address four fundamental questions: what should be separated, who should own each responsibility, how should new behavior be added, and what should depend on what.

alt text

Structure Is About Containing Change [Containing Change] {1}

A system can work perfectly today and still be badly designed for tomorrow. The real test comes when something changes. In a poorly structured system, one change spreads into unrelated parts; in a well-structured system, its impact stays close to where it belongs. Four ideas help contain change from different angles: separate concerns, give responsibilities clear ownership, extend without breaking what already works, and depend on abstractions rather than replaceable details. !!Good structure makes change local, predictable, and safe.!!

alt text

Separate What Changes for Different Reasons [Different Reasons] {1}

Different concerns should not be tangled together. Business rules, storage, presentation, security, and integration evolve for different reasons and at different speeds. Keeping them separate prevents a change in one concern from unnecessarily affecting the others. !!Separate concerns so each part can evolve without dragging unrelated parts with it.!!

alt text

Give Every Responsibility a Clear Owner [Clear Ownership] {1}

Once concerns are separated, each responsibility needs a clear home. When several responsibilities or stakeholders pull the same component in different directions, unrelated changes become coupled and ownership becomes ambiguous. !!A component should have one clear responsibility — and therefore one clear reason to change.!!

alt text

Extend Without Breaking What Works [Extend Safely] {1}

Systems need to grow, but growth should not require repeatedly rewriting stable code. Good structure creates extension points where new behavior can be added while existing behavior remains untouched. !!Growth should happen primarily by adding new behavior, not by destabilizing what already works.!!

alt text

Depend on What Matters, Not How It Is Done [Stable Abstractions] {1}

Business rules should describe what the system needs, not which database, framework, vendor, or protocol happens to implement it today. Replaceable technical choices should conform to the core rather than shape it. !!Depend on stable abstractions; keep replaceable details at the edges.!!

alt text

Keep Different Concerns Separate (Separation of Concerns) [Separate Concerns] {1}

Separation of Concerns (SoC) [Separation of Concerns] {1}

A system is easier to understand, change, and trust when each part handles one thing. !!Separation of Concerns is the first line of defense against the Big Ball of Mud!!: instead of letting a single piece of code juggle the user interface, the business rules, and the storage at once, SoC carves the system into units that each own a clear, narrow purpose. The payoff is concrete — you can change one concern without disturbing the others, you can test in isolation, and a new developer can read one unit without having to load the entire system in their head.

alt text

SoC at Every Scale [Every Scale] {1}

!!Separation of Concerns is not something you do once at the architecture level — it is a reflex that should accompany a developer throughout the day, at every scale of decision.!! One line should do one thing. One function should express one verb. One class should own one role. One module should cover one domain. One service should serve one bounded context. From the first variable named in the morning to the last service deployed at night, the same question keeps coming back: "is this unit doing one thing?"

alt text

Cohesion: The Other Half [Cohesion] {1}

Separating is only useful if what stays together belongs together. Cohesion is the silent partner of SoC: each separated unit must itself be tightly focused around a single purpose. Without cohesion, you do not get clean modules — you get a pile of fragments, each as confused as the original system, just smaller. !!The classic formulation captures both halves at once: high cohesion within, low coupling between!!. SoC fails the moment you optimize one without the other.

alt text

Boundaries Need Contracts [Boundaries Need Contracts] {1}

Separating concerns draws a line between two units — but a line alone is not a frontier. !!Without a contract, separation is just an illusion!!: the units keep talking through implicit assumptions, shared formats nobody wrote down, and constant coordination meetings to figure out what the other side really expects. A contract turns the line into a real boundary: each side knows exactly what it must deliver and what it can rely on, and both sides become autonomous — free to evolve internally as long as the contract holds. SoC creates the separation; the contract is what makes the separation hold.

alt text

Related chapter

Design for Testability shows how explicit contracts make isolated components and their integrations easier to verify.

SoC asks which concerns should be separated. SRP asks who should own each responsibility once they are separated.

Give Each Component One Responsibility (Single Responsibility Principle) [One Responsibility] {1}

Ownership Defines Architecture [Ownership Clarity] {1}

When components lack clear ownership, responsibilities blur. Different teams make conflicting changes, features land wherever convenient rather than where they belong, and the codebase becomes entangled — hard to understand, test, or modify with confidence. Well-defined ownership fixes this. It forces teams to think deliberately about where new functionality should reside, clarifies accountability, and keeps decision-making fast. Every module or service should have a clear organizational owner.

alt text

One Reason to Change: Single Responsibility Principle (SRP) [SRP] {1}

!!In a well-designed system, each component should have one clear reason to change, representing one group of stakeholders.!! This is known as the Single Responsibility Principle (SRP)

alt text

When multiple stakeholders, such as HR, Accounting, and Database Administration, influence a component, conflicting requirements create complexity, making the system harder to maintain and modify. For example, if a module handles payroll processing (Accounting), employee records (HR), and data storage (Database), a change requested by one group might unintentionally disrupt another, creating unnecessary dependencies and increasing maintenance costs.

alt text

By ensuring that each component has one well-defined responsibility, we create a modular, predictable, and adaptable system. This approach reduces unexpected side effects, simplifies debugging, and makes the system more scalable and maintainable over time.

SRP does not mean making every component as small as possible. Splitting responsibilities too far creates fragmentation, extra dependencies, and coordination overhead. Separate responsibilities when they change for different reasons — keep together what changes together.

Reducing Dependencies [SRP Dependencies] {1}

By constraining components, services, classes, or modules to one responsibility, we promote a clear and structured architecture. When each part of the system is responsible for a single concern, it becomes easier to understand what needs to change and why, reducing the risk of cascading failures. Without this structure, systems become tangled—where multiple elements depend on each other in ways that make even small changes risky and time-consuming. !!Instead of a spaghetti-like web of dependencies, SRP encourages a clean, loosely coupled design, where changes remain localized, and the system can evolve with minimal disruption.!!

alt text

Source: Clean Architecture: A Craftsman's Guide to Software Structure and Design

Extend Without Changing What Already Works (Open-Closed Principle) [Safe Extension] {1}

Open-Closed Principle (OCP) [OCP] {1}

!!Software should be designed so new functionality can be added without altering existing code.!! The Open-Closed Principle (OCP) captures this directly: a class, module, or component should be open for extension (new behaviors can be added) but closed for modification (existing code stays untouched). A well-designed structure — like a building — allows new floors or rooms to be added without tearing down what already works.

alt text

OCP in Code [OCP in Code]

In code, the mechanism is abstraction: well-defined interfaces, inheritance, or composition. Introducing new behavior means writing a new module, not rewriting the old one. In a reporting system, adding a new export format (CSV, PDF, XML) should mean adding a new export class — not editing the core with another if format_type == ... branch. This keeps regression risks low, preserves backward compatibility, and lets the system grow without becoming brittle. But extension points should follow real variation, not imagined futures. Applying OCP too early creates abstractions for changes that may never happen. Design for extension where change is expected or already visible — not everywhere.

alt text

Self-Describing Extensions [Self Describing]

When systems grow through plugins or adapters, a common mistake is to stash every extension's rules—permissions, concurrency, contracts, presentation—in one coordinator. That hub becomes a god object: each new extension forces edits to shared code and risks regressions far from the change. Better: keep each extension self-describing so the core does not need to know every name in the catalog.

alt text

The framework should offer generic mechanisms—discovery, schema validation, policy enforcement—not a growing list of special cases. A thin registry for wiring is fine; embedding extension-specific logic in the center is not. This extends the Open-Closed idea: new behavior ships mostly in the new module, not in an omniscient orchestrator.

alt text

Keep the Core Independent from the Details (Dependency Inversion Principle) [Core vs Details] {1}

Decoupling Implementation Details [Decouple Details] {1}

!!Business logic should be defined at the highest level of abstraction, focusing on what needs to be done, rather than how it is implemented.!! To ensure flexibility and maintainability, implementation details should remain hidden from the core business logic.

alt text

Dependency Inversion Principle (DIP) [DIP] {1}

!!This principle is called the Dependency Inversion Principle (DIP) because it reverses the control of dependencies.!! Before applying DIP, high-level business logic was directly coupled to low-level modules that it did not control. This created rigid dependencies, making it difficult to modify or replace implementations.

alt text

With DIP, the high-level module defines the contract (interface), and the low-level module must conform to it. Instead of business logic depending on specific implementations, both high- and low-level modules depend on a shared abstraction.

Code Against Contracts, Not Concretes [Code Against Contracts] {1}

Business logic acts as an orchestrator, coordinating operations that depend on external services — databases, payment systems, notifications. The rule is simple: keep replaceable implementations out of business logic. Talk to a contract instead — PaymentInterface, StockInterface, NotificationInterface — so any compatible implementation can be plugged in without changing the orchestration. Not every dependency needs an abstraction. If an implementation is simple, stable, and unlikely to be replaced, adding an interface may only add indirection. Invert dependencies where replaceability, testing, or change justifies the cost.

alt text

From Implementation to Abstraction [From Implementation to Abstraction]

The shift is small in code but huge in architecture. Before DIP, OrderService is tightly coupled to NotificationEmailService — change the email vendor, change the order code. After DIP, both OrderService and NotificationEmailService depend on the same abstract contract: the high-level no longer points at the low-level, they both point at the interface in the middle.

alt text

Dependency Injection at Runtime [Dependency Injection]

Once business logic depends only on abstractions, someone still has to provide the real implementation. That responsibility moves outside the business logic — to a dependency injection container, a framework, or a wiring layer. At runtime, this external mechanism creates the concrete services and injects them into the orchestrator. The business logic stays clean; the plumbing lives elsewhere. Dependency injection is one way to wire these dependencies at runtime; it supports DIP, but it is not the principle itself.

alt text

If You Can Replace It, It's a Detail [Replaceable Detail] {1}

In a great orchestra, each musician can be replaced — what truly matters is the melody. !!Software is the same: anything you can swap without rewriting the business rules is, by definition, a detail.!! The database, the framework, the queue, the protocol — they are interchangeable instruments. The melody is the architecture.

alt text

Core vs Details [Core vs Details] {1}

Most teams get the priorities backwards. They start every project by picking the database, the framework, the cloud provider — and only then write the business rules around them. But those choices are details. The core is the part that cannot be replaced without changing what the product does: business rules, domain logic, data model. !!Everything else conforms to the core, never the reverse.!!

alt text

Do Not Let Details Drive Architectural Decisions [Details vs. Architecture] {1}

The clearest sign that DIP is being violated is when implementation details leak directly into the architecture: a raw SQL query in business code, a framework annotation in a domain rule, a vendor SDK class in a core service. !!Technologies are tools, not pillars — when they dictate the architecture, the system becomes rigid, brittle, and tied to choices that will outlive their relevance.!! Good design pushes details to the edge and keeps the core clean.

alt text

Twisting a Good Design to Fit New Details [Design Twist] {1}

When new technical requirements or constraints emerge, there's often pressure to modify a well-designed architecture to accommodate them. This "twisting" of good design to fit new details is a dangerous practice that compromises architectural integrity. Instead of adapting the design to fit the details, the better approach is to evaluate whether the new details are truly necessary, find alternative solutions that don't require architectural changes, or refactor the design properly to accommodate the new requirements. !!Twisting a design often leads to technical debt, increased complexity, and systems that become harder to maintain and extend.!! The key is to resist the temptation of quick fixes and instead invest in proper architectural solutions that maintain design quality while meeting new requirements.

alt text

If You Can Replace Details Easily, You Can Test It Easily Too [Testability]

A well-designed system allows implementation details to be replaced easily, making testing more effective and maintainable. By using mocks and stubs, we can isolate business logic from low-level dependencies, ensuring that tests focus on validating rules rather than technical details. This approach leads to stable tests that remain valid even if the database, framework, or API changes. When tests are tightly coupled to implementation details, they become fragile and require constant updates, defeating their purpose. Instead, designing for replaceability ensures that tests remain resilient, fast, and focused on verifying the core functionality rather than the underlying mechanisms.

alt text

Related chapter

Design for Testability explores test seams, mocks, contracts, and the broader testing strategy that dependency inversion enables.