SOLID Principles in Backend Design

Day 056: SOLID Principles in Backend Design

SOLID is useful when it stops being a mnemonic and starts acting like a diagnostic tool for where backend code is becoming hard to change safely.


Today's "Aha!" Moment

SOLID is usually taught as five separate principles to memorize. That is why many students either over-respect it or ignore it. In real backend work, SOLID is more useful as a compact list of failure modes. A class is changing for too many reasons. A new feature always means editing one fragile conditional block. A substitute implementation compiles but behaves differently. An interface drags unrelated methods into every consumer. Domain logic imports concrete infrastructure directly. Those are the real problems behind the letters.

Use the learning platform's enrollment flow. At the beginning, one service can get away with doing everything. Then product asks for discount rules, payment retries, instructor overrides, audit logging, and a different notification path for enterprise customers. Nothing seems catastrophic at first. The service just grows. But over time it becomes harder to tell which changes belong together and which parts should be allowed to vary independently.

That is the aha. SOLID is not a style guide for making more classes. It is a way to ask better design questions under change pressure. What responsibilities are colliding in one module? Where should new behavior plug in without rewriting the core? Which contracts are trustworthy enough to substitute? Which dependencies are too concrete for policy code to depend on? If the lesson lands there, the five letters stop feeling artificial.


Why This Matters

The problem: Backend code usually does not become painful because of one huge mistake. It becomes painful because the code shape stops matching the way the product changes.

Before:

After:

Real-world impact: This improves refactoring safety, makes team ownership boundaries cleaner, and reduces the chance that one feature request destabilizes unrelated backend behavior.


Learning Objectives

By the end of this session, you will be able to:

  1. Read SOLID as a set of backend failure modes - Explain what each principle is trying to prevent.
  2. Spot the design pressure behind the principle - Identify when mixed responsibility, brittle extension, or bad contracts are the real problem.
  3. Apply the principles without ceremony - Use SOLID to reshape code where the current change pattern justifies it.

Core Concepts Explained

Concept 1: SRP and OCP Are Really About Shaping the Code Around Change

Single Responsibility Principle and Open/Closed Principle are often presented separately, but in practice they reinforce each other. The first asks whether a module is being pulled by unrelated reasons to change. The second asks whether the next variation can be added without reopening a fragile center of logic every time.

Take EnrollmentService. If it decides enrollment eligibility, calculates discounts, formats customer-facing emails, chooses retry policy, and writes audit records directly, then different teams and different product changes are all pulling on the same class. That is the SRP problem. Once that happens, every new variant also tends to land as another if or switch inside the same hotspot. That is the OCP problem.

enrollment workflow      -> should stay central
discount policy          -> should vary independently
notification strategy    -> should vary independently
storage mechanics        -> should vary independently

A healthier shape is to keep the workflow orchestration in one place and move the changing dimensions behind clearer seams. Maybe discount rules become pluggable policy objects. Maybe notification delivery is delegated. Maybe repository access is already behind a boundary. The goal is not endless indirection. The goal is to stop unrelated changes from colliding in one file.

The trade-off is upfront structure versus future blast radius. A little more shape now can save many risky edits later, but over-applying it before the pressure is real can also produce dead abstractions.

Concept 2: LSP and ISP Protect Contract Quality, Not Inheritance Purity

Liskov Substitution Principle is often taught through inheritance trivia, but its practical backend meaning is simpler: if a component claims to satisfy a contract, the consumer should not need special-case knowledge about which implementation it got. Interface Segregation Principle complements that by saying the contract should not be bigger than the consumer's real need.

Imagine the enrollment workflow depends on a PaymentGateway contract. A real Stripe adapter and a test fake can both satisfy that contract, but only if they preserve the same important semantics. If the fake silently accepts impossible states or never raises the failures the real gateway can raise, the tests become reassuring in exactly the wrong way. That is an LSP failure.

Now imagine every consumer depends on one giant PlatformServices interface with methods for charging payments, creating users, sending emails, exporting reports, and invalidating caches. Most consumers need only one or two of those capabilities. That is an ISP failure.

class EnrollmentPayments:
    def charge(self, learner_id, amount):
        raise NotImplementedError


class EnrollmentNotifier:
    def send_confirmation(self, learner_id, course_id):
        raise NotImplementedError

The point is not that every interface must be microscopic. The point is that contracts should be small enough to be trustworthy and specific enough to preserve behavior. A consumer that depends on a narrow, meaningful capability is easier to test and easier to substitute correctly.

The trade-off is more explicit contract design in exchange for safer substitution. Poorly shaped contracts create accidental coupling; over-fragmented contracts create noise. The right size is the smallest boundary that still represents a coherent capability.

Concept 3: DIP Keeps Policy Above Details, and DI Is One Way to Realize It

Dependency Inversion Principle is the one that ties the rest together architecturally. Higher-level policy code should not depend directly on volatile details such as ORM models, HTTP SDKs, or concrete queue clients. It should depend on abstractions that describe what it needs from the outside world.

That should sound familiar now, because it connects directly to the previous lessons. Repositories are one example of dependency inversion applied to persistence. Dependency injection is one common way to wire those abstractions to concrete implementations at runtime. The principle is not "use interfaces everywhere." The principle is "do not let the core behavior point inward toward details that change more often than the policy."

policy / use case
      |
      v
stable contract
      ^
      |
adapter / framework / infrastructure detail

This matters because it changes what the core of the backend is allowed to know. Enrollment policy should know that it can charge a learner. It should not need to know which SDK class was imported, how the HTTP client is configured, or how the ORM session is scoped.

The trade-off is abstraction versus immediacy. Depending directly on a concrete implementation is quicker in the moment. Depending on a meaningful contract keeps the core stable when frameworks, providers, or tests change around it.


Troubleshooting

Issue: Using SOLID as a reason to create abstractions before any real change pressure exists.

Why it happens / is confusing: The principles are taught as always-good rules, so it is easy to mistake them for a mandate to generalize early.

Clarification / Fix: Start with the current maintenance problem. If nothing is colliding yet, keep the code simple. Add structure when the code shape is clearly mismatched to how it is changing.

Issue: Thinking LSP is only about class inheritance, not about behavioral trust.

Why it happens / is confusing: Many examples use geometric shapes and inheritance trees instead of realistic adapters, fakes, and backend contracts.

Clarification / Fix: Ask whether the consumer can keep the same assumptions when the implementation changes. If not, the substitution contract is broken even if the types line up.


Advanced Connections

Connection 1: SOLID ↔ Layered Architecture

The parallel: Layered architecture separates big categories of concern; SOLID helps shape the internals of each layer so those boundaries stay meaningful over time.

Real-world case: A thin controller, focused use case, and repository boundary usually reflect SRP and DIP working together instead of one giant service doing everything.

Connection 2: SOLID ↔ Repositories and Dependency Injection

The parallel: Repositories are one application of DIP to persistence, and dependency injection is one common way to realize that dependency direction at runtime.

Real-world case: An enrollment workflow that depends on repository and payment contracts can stay stable while tests, storage adapters, or providers vary around it.


Resources

Optional Deepening Resources


Key Insights

  1. SOLID is a lens on change pressure - Each principle names a recurring way backend code becomes harder to evolve.
  2. Contracts matter as much as decomposition - Bad substitution and bloated interfaces can break a design even when responsibilities look split.
  3. DIP ties the set together - Keeping policy above details is what lets the rest of the design stay flexible without collapsing into framework-driven code.

Knowledge Check (Test Questions)

  1. What is the most useful way to read SOLID in backend work?

    • A) As a set of failure modes around change, coupling, and contracts.
    • B) As a requirement to split every class until it has only one method.
    • C) As a style guide that only matters in inheritance-heavy codebases.
  2. What is a strong sign that LSP is being violated in a backend design?

    • A) A substitute implementation matches the type but breaks assumptions the consumer relies on.
    • B) An implementation uses composition instead of inheritance.
    • C) A contract has only one method.
  3. What is the practical goal of DIP in a backend?

    • A) To keep higher-level policy code from depending directly on unstable infrastructure details.
    • B) To ensure every dependency is an interface, whether it helps or not.
    • C) To remove the need for wiring at application startup.

Answers

1. A: SOLID is most useful when you read it as a compact map of why code becomes rigid or misleading under real product evolution.

2. A: LSP is about behavioral trust. If the consumer must know which implementation it got in order to stay correct, the substitution boundary is weak.

3. A: DIP protects the core of the backend from framework and provider churn by keeping policy dependent on more stable contracts.



← Back to Learning