SOLID Design Principles

Problem framing

Distributed systems run on evolving service codebases, shared libraries, and SDKs that many teams depend on. As requirements change, tight coupling turns small updates into risky, cross-service changes. SOLID provides code-level constraints that keep services adaptable and testable as architectures scale. For service boundary choices and scaling context, see compute patterns.

Core idea / pattern

SOLID is a set of dependency and responsibility rules that reduce change ripple. Each principle frames a problem, prescribes a structural pattern, and clarifies the trade-offs.

Single Responsibility Principle (SRP)

Problem: One module handles multiple concerns, so unrelated changes collide.

Pattern: Split responsibilities so each module has one reason to change.

Trade-offs: More components to manage and wire together.

Failure modes: Over-splitting creates chatty modules and unclear ownership.

Example: Separate pricing calculation from invoice delivery.

class PricingService { calculate(order) }
class InvoiceSender { send(invoice) }

Open/Closed Principle (OCP)

Problem: New behaviors require editing stable, high-risk code paths.

Pattern: Extend behavior via new modules or strategies instead of modifying core logic.

Trade-offs: Strategy selection logic can become complex without governance.

Failure modes: Plugin sprawl hides critical behavior behind indirect wiring.

Example: Add a new shipping rule without touching the core dispatcher.

interface ShippingRule { quote(order) }
class ExpeditedRule implements ShippingRule { ... }

Liskov Substitution Principle (LSP)

Problem: Subtypes violate the expectations of their base types.

Pattern: Ensure derived types preserve contract behavior and invariants.

Trade-offs: Some inheritance hierarchies become impossible or require redesign.

Failure modes: Runtime failures appear only in specific subtype deployments.

Example: A read-only repository should not inherit from a write-capable base.

interface ReadRepo { find(id) }
interface WriteRepo extends ReadRepo { save(entity) }

Interface Segregation Principle (ISP)

Problem: Clients depend on large interfaces they only partially use.

Pattern: Provide smaller, role-specific interfaces.

Trade-offs: More interfaces to discover and document.

Failure modes: Excessive segregation leads to duplication and confusion.

Example: Split read APIs from admin APIs for analytics clients.

interface MetricsReader { query(range) }
interface MetricsAdmin { backfill(job) }

Dependency Inversion Principle (DIP)

Problem: High-level policies depend directly on low-level vendor details.

Pattern: Depend on abstractions; implement adapters at the edges.

Trade-offs: Extra indirection and the need for stable contracts.

Failure modes: Weak abstractions leak vendor semantics into core logic.

Example: A billing service depends on a gateway interface, not a single provider SDK.

interface PaymentGateway { charge(amount) }
class StripeGateway implements PaymentGateway { ... }
Principle Primary focus Distributed-systems benefit Quick example
SRP One reason to change Smaller deploy blast radius inside services Split validation from enrichment
OCP Extend without edits Safer feature rollouts and canaries Add a new routing rule
LSP Subtype safety Predictable behavior across deployments Writable vs read-only stores
ISP Small interfaces Less coupling across service boundaries Separate reader and admin APIs
DIP Abstraction-first dependencies Vendor swaps without core rewrites Gateway adapter for payments

Architecture diagram

flowchart LR
  Client[API Handler] --> UseCase[Use Case Service]
  UseCase --> Port[Domain Port Interface]
  UseCase --> Policy[Policy Engine]
  Port --> Adapter[Repository Adapter]
  Adapter --> DB[(Database)]
  UseCase --> Notifier[Notifier Interface]
  Notifier --> Email[Email Adapter]
        

Step-by-step flow

  1. Map change hotspots and identify mixed responsibilities.
  2. Split modules into focused components with clear ownership.
  3. Define small interfaces for each client role.
  4. Invert dependencies so policies depend on interfaces, not vendors.
  5. Swap adapters per environment and verify behavior via tests.

Failure modes

Trade-offs

Real-world usage