Featured

SOLID PRINCIPLES

SOLID Design Principles for Senior Python Engineers

CLEAN ARCHITECTURE SERIES

SOLID Principles in Python: The Senior Playbook

Series: Architecture & Scale
Day 14 / 100
Level: Staff Engineer

To master the system, you must first master its boundaries. In production-scale systems, applying SOLID isn't about dogmatic academic rules. It is about managing cognitive load, isolating failure domains, and ensuring codebases evolve without cascading regression failures.

Context: When working in Python—a highly dynamic, multi-paradigm language—SOLID architecture looks different than it does in compiled, static languages like Java or C++. We leverage structural subtyping (protocols), runtime decoupling, and dynamic interfaces while maintaining strict type safety to write code that scales with team size and infrastructure demands.

1. Single-Responsibility Principle (SRP)

The single-responsibility principle states that "a class should have only one reason to change." In the real world, this is a measure of cohesion. If a single class contains database serialization logic, business validations, and email delivery rules, a database schema modification risks breaking your notification routing system.

By splitting state from behavioral services, we isolate distinct axes of change and make unit testing highly direct and mock-free.

SRP: Isolating Invariants from Infrastructure Details
from dataclasses import dataclass
from decimal import Decimal

# 1. State Representation (Encapsulates data validation invariants)
@dataclass(frozen=True)
class Order:
    order_id: str
    total_amount: Decimal
    customer_email: str

    def __post_init__(self):
        if self.total_amount <= Decimal("0.00"):
            raise ValueError("Order total must be greater than zero.")

# 2. Storage Handler Service (Responsible only for DB writes)
class OrderRepository:
    def save(self, order: Order) -> None:
        print(f"Saving order {order.order_id} to database...")

# 3. Notification Service (Responsible only for communications)
class NotificationEngine:
    def send_receipt(self, order: Order) -> None:
        print(f"Sending receipt to {order.customer_email}...")

2. Open-Closed Principle (OCP)

Software modules must be "open for extension, but closed for modification." If supporting a new delivery method or a third-party gateway forces you to open your core transaction engine and write an elif gateway == "paypal": block, your system violates OCP.

In Python, we satisfy this beautifully using typing.Protocol, allowing polymorphism to seamlessly route requests to newly compiled extensions without mutating legacy orchestrators.

OCP: Abstracting Payment Strategies via Protocols
from typing import Protocol

# Open for extension: anyone can implement this protocol
class PaymentStrategy(Protocol):
    def charge(self, amount: Decimal) -> str: ...

# Closed for modification: Core engine requires no changes for new gateways
class CheckoutService:
    def __init__(self, processor: PaymentStrategy):
        self.processor = processor

    def complete_checkout(self, total: Decimal) -> None:
        tx_id = self.processor.charge(total)
        print(f"Checkout completed successfully. TX: {tx_id}")

# Concrete Extensions can be declared anywhere without altering CheckoutService
class StripePayment:
    def charge(self, amount: Decimal) -> str:
        return f"stripe_charge_{amount}"

"The master architect structures the void, not by adding walls, but by securing the thresholds.
When interfaces are clean and contracts are absolute, details may shift endlessly, yet the core remains untouched."

— Meditations on Clean Architecture

3. Liskov Substitution Principle (LSP)

Subtypes must be completely substitutable for their parent types. If a subclass narrows the input parameters, yields unexpected empty defaults (e.g. returning None instead of a collection), or leaks unique, unhandled errors, it violates LSP.

A classic violation in Python occurs when wrapping external SDK clients. If one gateway raises a Stripe-specific error up the call stack, calling layers must declare defensive try-except blocks specific to that subclass, destroying polymorphism.

LSP: Protecting API Contracts and Exception Uniformity
class PaymentFailed(Exception): pass

class BaseGateway:
    def pay(self, amount: Decimal) -> None:
        raise NotImplementedError

class AdyenGateway(BaseGateway):
    def pay(self, amount: Decimal) -> None:
        try:
            # Low-level network operations here...
            raise ConnectionResetError("Adyen dropped connection")
        except Exception as err:
            # LSP preserved: translate low level library errors to Domain Exceptions
            raise PaymentFailed("Adyen transaction aborted") from err

4. Interface Segregation Principle (ISP)

Clients should not be forced to depend upon interfaces they do not use. Massive, monolithic interfaces force subclasses to implement empty placeholder logic or raise a NotImplementedError, indicating bad structural design.

In Python, we achieve ISP by declaring small, laser-focused Protocols (often with a single method) and relying on structural composition if a composite interface is required.

ISP: Building Lean, Single-Method Structural Protocols
class Readable(Protocol):
    def read(self) -> bytes: ...

class Writable(Protocol):
    def write(self, data: bytes) -> None: ...

# Composite Interface: Constructed simply by merging lean protocols
class ReadWritable(Readable, Writable, Protocol):
    pass

# Read-only utility depends ONLY on the Readable interface
class DataProcessor:
    def __init__(self, source: Readable):
        self.source = source

    def parse(self) -> str:
        raw_data = self.source.read()
        return raw_data.decode("utf-8")

5. Dependency Inversion Principle (DIP)

High-level business policies should not depend on low-level utility implementations. Both should depend on abstractions. Tightly coupling orchestrators directly to specific database drivers or network clients introduces extreme friction during updates or test suites execution.

By injecting interface protocols during application bootstrap, we decouple high-level workflows from infrastructure details entirely.

DIP: Decoupled High-Level Orchestrator
# Abstraction defined inside the High-Level Application boundary
class DataStore(Protocol):
    def retrieve_data(self) -> dict: ...

# High-level Orchestrator depends strictly on the abstraction
class AnalyticsDashboard:
    def __init__(self, store: DataStore):
        self.store = store

    def render(self) -> None:
        payload = self.store.retrieve_data()
        print(f"Rendering dashboard metrics with: {payload}")

# Low-level detail implements the high-level contract
class PostgresDataStore:
    def retrieve_data(self) -> dict:
        return {"metric_a": 100.50, "metric_b": 200.12}

🛠️ SOLID Synthesis: The Pragmatic Architect

A Senior Developer understands when *not* to over-engineer. Dogmatically implementing all five principles on a single-file, short-lived script will only inflate cognitive overhead and complexity without yielding value. Apply abstractions when you recognize a second concrete implementation emerging, or when the cost of testing mixed-concern classes begins to decelerate your deployment pipelines.

🔥 PRO UPGRADE / TEASER

We have conquered structural clean code at the class level. Next, we will bridge these boundaries across microservices. Tomorrow, we dissect the performance and patterns of asynchronous message brokers. Welcome to Day 15: The Event-Driven Frontier.

Architectural Consulting

Do you need an expert to untangle legacy code structures and establish resilient API design boundaries in your cloud ecosystems? Let's build stable software that lasts.

Explore Enterprise Engagements →


















Comments