Production-grade guide to solid principles production covering architecture patterns, implementation strategies, testing approaches, and operational best practices for enterprise engineering teams.
This reference describes how to apply the SOLID principles in production software systems, focusing on practical implementation, deployment, and failure modes. These are the foundational design rules for maintainable, scalable, and robust codebases.
The Single Responsibility Principle (SRP) requires that each class or module should have only one reason to change. When implementing this principle, verify that a module's responsibilities align with its usage patterns.
A common failure occurs when modules grow beyond their original purpose. For example, consider a service class that both manages database connections and performs business logic:
class OrderService:
def __init__(self):
self.db = DatabaseConnection()
def process_order(self, order_data):
# Business logic here
return self.db.save(order_data)
def get_user_orders(self, user_id):
# Another responsibility
return self.db.query(f"SELECT * FROM orders WHERE user_id = {user_id}")
This class violates SRP because it handles both persistence and business operations. The fix involves separating concerns into distinct classes:
class OrderRepository:
def __init__(self):
self.db = DatabaseConnection()
def save(self, order_data):
return self.db.save(order_data)
def find_by_user(self, user_id):
return self.db.query(f"SELECT * FROM orders WHERE user_id = {user_id}")
class OrderService:
def __init__(self, repository):
self.repository = repository
def process_order(self, order_data):
# Business logic only
return self.repository.save(order_data)
When deploying services that follow SRP, ensure configuration tools do not allow cross-cutting concerns to creep into single modules. Monitor for code changes that add new responsibilities to existing classes.
The Open/Closed Principle states that software entities should be open for extension but closed for modification. This is typically implemented using inheritance, composition, or strategy patterns.
In practice, this means designing interfaces that allow new behavior without altering existing code:
class PaymentProcessor:
def process(self, amount):
raise NotImplementedError
class CreditCardProcessor(PaymentProcessor):
def process(self, amount):
# Implementation for credit card
return f"Charged ${amount} via credit card"
class PayPalProcessor(PaymentProcessor):
def process(self, amount):
# Implementation for PayPal
return f"Charged ${amount} via PayPal"
# Usage
def charge_customer(processor, amount):
return processor.process(amount)
When deploying this pattern, ensure that the system can dynamically load new implementations without recompilation. Many systems use dependency injection containers or service registries to manage such extensions.
The most common failure mode is treating the principle as a static design decision. Production systems must account for how extension points are managed at runtime. If a new payment method requires database schema changes, the system may not be truly open for extension.
The Liskov Substitution Principle (LSP) requires that objects of a superclass should be replaceable with instances of its subclasses without altering the correctness of the program. This is particularly important in production when polymorphic behavior is expected.
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Square(Rectangle):
def __init__(self, side):
super().__init__(side, side)
def set_width(self, width):
self.width = width
self.height = width # This violates LSP
The Square class breaks LSP because setting width affects height, which changes the object's behavior. Production systems often fail to detect this when testing with concrete classes rather than interfaces.
A robust implementation maintains behavioral consistency:
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
@property
def width(self):
return self._width
@width.setter
def width(self, value):
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
self._height = value
def area(self):
return self._width * self._height
class Square(Rectangle):
def __init__(self, side):
super().__init__(side, side)
@property
def width(self):
return self._width
@width.setter
def width(self, value):
self._width = value
self._height = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
self._height = value
self._width = value
In production, test that all subclasses behave identically under the same interface. This is critical for systems using polymorphic operations.
Interface Segregation Principle (ISP) states that clients should not be forced to depend on interfaces they don't use. In production, this prevents cascading changes when interfaces evolve.
# Bad example - fat interface
class Worker:
def work(self):
pass
def eat(self):
pass
def sleep(self):
pass
class HumanWorker(Worker):
def work(self):
return "Working"
def eat(self):
return "Eating"
def sleep(self):
return "Sleeping"
class RobotWorker(Worker):
def work(self):
return "Working"
def eat(self):
raise NotImplementedError("Robots don't eat")
def sleep(self):
raise NotImplementedError("Robots don't sleep")
The correct approach splits interfaces:
class Workable:
def work(self):
pass
class Eatable:
def eat(self):
pass
class Sleepable:
def sleep(self):
pass
class HumanWorker(Workable, Eatable, Sleepable):
def work(self):
return "Working"
def eat(self):
return "Eating"
def sleep(self):
return "Sleeping"
class RobotWorker(Workable):
def work(self):
return "Working"
In production, this separation prevents changes to eating behavior from affecting robot workers. This is especially important when services communicate through interfaces and need to scale independently.
The Dependency Inversion Principle (DIP) requires that high-level modules should not depend on low-level modules. Both should depend on abstractions. This is essential for decoupling components in production systems.
# Bad example - direct dependencies
class EmailService:
def send_email(self, message):
# Implementation details
pass
class NotificationService:
def __init__(self):
self.email_service = EmailService() # Direct dependency
def notify_user(self, user_id, message):
return self.email_service.send_email(message)
The correct implementation uses abstractions:
class NotificationSender:
def send(self, message):
raise NotImplementedError
class EmailService(NotificationSender):
def send(self, message):
# Implementation details
return f"Email sent: {message}"
class NotificationService:
def __init__(self, sender: NotificationSender):
self.sender = sender
def notify_user(self, user_id, message):
return self.sender.send(message)
Production systems using DIP must ensure that dependency injection works correctly. Configuration management tools should allow swapping implementations without redeployment.
The most common production failure is interface evolution that silently breaks clients. When a method signature changes, existing implementations may continue to work but behave incorrectly:
# Before
class DataProcessor:
def process(self, data):
return data.upper()
# After - breaking change
class DataProcessor:
def process(self, data, options=None):
if options and options.get('uppercase', True):
return data.upper()
return data.lower() # Silent behavior change
Many production systems assume compile-time checking for interface compliance. In languages without strong typing, runtime failures can occur:
# Python example - runtime failure
def process_items(items):
for item in items:
if hasattr(item, 'process'):
item.process() # No guarantee that process method exists
else:
raise AttributeError("Item must have process method")
Dependency inversion requires robust configuration. When dependencies are configured incorrectly, production systems fail silently:
# Configuration example - missing dependency
services:
notification_service:
type: email
# Missing required configuration for email service
When deploying systems following SOLID principles, ensure that configuration tools support runtime dependency swapping. Monitor for violations of these principles in deployed code.
Use continuous integration to catch interface changes that break existing implementations. Test both compile-time and runtime behavior to ensure all dependencies are properly injected.
Testing production code that follows SOLID principles requires careful attention to polymorphic behavior:
import unittest
class TestNotificationService(unittest.TestCase):
def test_email_notification(self):
sender = EmailService()
service = NotificationService(sender)
result = service.notify_user(123, "Hello")
self.assertEqual(result, "Email sent: Hello")
def test_different_senders(self):
# Test that any NotificationSender works
senders = [EmailService(), SMSNotificationService()]
for sender in senders:
service = NotificationService(sender)
result = service.notify_user(123, "Hello")
self.assertTrue(result.startswith("Sent"))
Production testing must verify that all implementations of interfaces behave correctly under the same contract. This prevents subtle failures when production code switches between different implementations.
Production systems using SOLID principles benefit from monitoring that detects interface violations:
# Logging example
class MonitoredService:
def __init__(self, sender):
self.sender = sender
def notify_user(self, user_id, message):
try:
result = self.sender.send(message)
# Log successful notification
return result
except Exception as e:
# Log interface violation
logger.error(f"Notification failed: {e}")
raise
Production monitoring should detect when implementations fail to meet expected contracts. This includes checking that all required methods exist and behave correctly.
Applying SOLID principles in production requires careful attention to how interfaces are defined, implemented, and managed. The key is ensuring that systems remain flexible while maintaining predictable behavior. Production failures typically occur from ignoring these principles during development, leading to systems that appear correct but fail under real-world conditions.
This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy