Skip to content

Design Principles: Beyond SOLID

SOLID is not the whole story. These ideas work in any language. They reduce accidental complexity and make systems easier to change safely.


1.2 DRY — Don't Repeat Yourself

Every piece of knowledge should have one clear, authoritative place in the system. When the rule changes, you change it once.

Common duplication:

  • Code duplication: the same logic copied in two files. One update can be missed.
  • Logic duplication: the same rule written twice (for example validation in a browser and on the server) without a shared definition.
  • Schema duplication: the same structure in the database, API contract, and Python model, all edited by hand.

Fix: centralize — one validator, one model source, one place that owns the rule.

Caution: DRY is not “merge every similar-looking function.” If two pieces look the same but change for different reasons, one shared function ties them together (bad coupling). Ask: Do they change together?


1.3 KISS — Keep It Simple, Stupid

Pick the simplest design that solves the problem. Complexity is a tax on every future reader.

Minimal abstraction: add layers or patterns when you have a concrete need (several implementations, testing seams, real reuse). Do not add abstraction “for later.”

Warning signs:

  • You cannot explain the design in one short sentence.
  • New developers need a long time to understand one function.
  • A trivial task passes through many indirection layers.

1.4 YAGNI — You Aren't Gonna Need It

Do not build features or general solutions for requirements that do not exist yet. Build what you need now. Extend when a real need appears.

Cost of guessing: extra code to read, test, and maintain — and the feature may never be used.

Practical rule: no ticket, no agreed user story, no real stakeholder need — do not implement it.


1.5 Composition over Inheritance

Prefer building behavior from small parts (composition) over deep class trees (inheritance).

Why inheritance hurts at scale:

  • Deep hierarchies are hard to follow.
  • A change in a base class can break many subclasses.
  • Subclasses inherit everything, including behavior they do not want.

Composition example:

class Logger:
    def log(self, msg: str) -> None:
        ...

class MetricsCollector:
    def record(self, name: str, value: float) -> None:
        ...

class PaymentService:
    def __init__(self, logger: Logger, metrics: MetricsCollector) -> None:
        self._logger = logger
        self._metrics = metrics

    def charge(self, amount: float) -> None:
        self._logger.log(f"charge {amount}")
        self._metrics.record("payment_amount", amount)

PaymentService uses Logger and MetricsCollector; it does not inherit from them.


1.6 Separation of Concerns (SoC)

Different concerns live in different places. A concern is a distinct responsibility: HTTP, validation, business rules, persistence, logging.

Typical layers:

Layer Role
Controller Parse input, call application code, shape the response.
Service Business rules and orchestration.
Repository Data access.
Middleware Cross-cutting: auth, logging, rate limits.

Mixing: a controller that runs raw SQL. A domain service that builds HTTP headers. Both blur boundaries and make tests and changes harder.


1.7 High Cohesion / Low Coupling

High cohesion: everything in a module belongs together for one clear purpose. Easier to read, test, and name.

Low coupling: modules depend on each other lightly. A change in module A should not force large edits across module B.

Practical checks:

  • How many imports does this file need?
  • If you change this module, how many others break?
  • Can you test this module with mocks or fakes instead of the whole system?

Goal: modules you can understand, test, and sometimes deploy or replace with less pain.