The God Class Problem
SOLID principles aren't about memorizing definitions or using fancy keywords. They're about writing code that doesn't make you want to cry when you revisit it six months later.
In today's age of "vibe coding" where AI generates hundreds of lines in seconds, we're out of excuses. We have the tools to write clean, testable code—we just need to prompt AI correctly. SOLID isn't optional anymore; it's the difference between shipping fast AND maintaining sanity.
Think of SOLID as five common-sense rules:
- Single Responsibility: One class, one job
- Open/Closed: Add features without breaking existing code
- Liskov Substitution: Swap implementations without things exploding
- Interface Segregation: Small, focused contracts
- Dependency Inversion: Depend on abstractions, not concrete implementations
The Problem: A Real-World Nightmare
Here's a typical e-commerce order processor that violates every SOLID principle:
class OrderProcessor {
constructor() {
this.orders = [];
}
processOrder(customerData, items, paymentType, shippingAddress) {
// Validates customer
if (!customerData.name || !customerData.email) {
throw new Error("Invalid customer");
}
// Calculates total
let total = 0;
for (let item of items) {
total += item.price * item.quantity;
}
// Applies discount
if (customerData.isPremium) {
total *= 0.9;
}
// Processes payment
if (paymentType === "credit") {
console.log("Processing credit card...");
} else if (paymentType === "paypal") {
console.log("Processing PayPal...");
} else if (paymentType === "bitcoin") {
console.log("Processing Bitcoin...");
}
// Calculates shipping
let shippingCost = 0;
if (shippingAddress.country === "US") shippingCost = 10;
else if (shippingAddress.country === "CA") shippingCost = 15;
else shippingCost = 25;
// Sends email
console.log("Sending email to: " + customerData.email);
// Saves to database
this.orders.push({ customerData, items, total });
// Generates invoice
console.log("=== INVOICE ===");
console.log("Total: $" + (total + shippingCost));
}
}
What's Wrong?
This class isn't just a God Class—it's the entire pantheon. It's doing validation, calculation, payment processing, shipping, emails, database operations, and invoice generation. Want to add Apple Pay? Touch this monolith. Want to test payments? Good luck mocking everything else.
Requirements always change. Usually at 4 PM on Friday. Right before a demo.
The reality: A "2-hour feature" becomes 6 hours because changing one thing breaks three others.
The Refactoring Journey
Attempt 1: Extract Methods (15% there)
class OrderProcessor {
processOrder(data) {
this.validateCustomer(data.customer);
const total = this.calculateTotal(data.items);
this.processPayment(total, data.paymentType);
}
validateCustomer(customer) { /* ... */ }
calculateTotal(items) { /* ... */ }
processPayment(total, type) { /* ... */ }
}
Problem: Still one giant class. Can't test parts independently. Just organized chaos.
Attempt 2: Separate Classes (30% there)
class CustomerValidator {
validate(customer) { /* ... */ }
}
class PaymentProcessor {
process(amount, type) {
if (type === "credit") { /* ... */ }
else if (type === "paypal") { /* ... */ }
}
}
Problem: PaymentProcessor still has that if-else chain. Adding new payment types means modifying this class.
Attempt 3: Abstractions! (85% there)
abstract class Payment {
abstract processPayment(total: number): void;
}
class CreditPayment extends Payment {
processPayment(total: number): void {
console.log(`Processing credit card: $${total}`);
if (total > 10000) throw new Error("Limit exceeded");
}
}
class PayPalPayment extends Payment {
processPayment(total: number): void {
console.log(`Processing PayPal: $${total}`);
}
}
Win: Each payment type is its own class. Want to add Apple Pay? Create ApplePayPayment without touching existing code.
Apply the same pattern to shipping, discounts, and notifications:
abstract class ShippingCalculator {
abstract calculateShippingCost(address: Address): number;
}
abstract class DiscountStrategy {
abstract applyDiscount(total: number): number;
}
abstract class NotificationService {
abstract sendNotification(message: string): void;
}
The Final Solution
Here's the refactored architecture:
// Core abstractions
abstract class Payment {
abstract processPayment(total: number): void;
}
abstract class ShippingCalculator {
abstract calculateShippingCost(address: Address): number;
}
abstract class DiscountStrategy {
abstract applyDiscount(total: number): number;
}
// Concrete implementations
class CreditPayment extends Payment {
processPayment(total: number): void {
console.log(`Processing credit card: $${total}`);
}
}
class USShipping extends ShippingCalculator {
calculateShippingCost(address: Address): number {
return 10;
}
}
class PremiumDiscount extends DiscountStrategy {
applyDiscount(total: number): number {
return total * 0.9;
}
}
// The orchestrator (simplified)
class OrderProcessor {
constructor(
private validator: CustomerValidator,
private calculator: OrderCalculator,
private repository: OrderRepository,
private invoiceGenerator: InvoiceGenerator
) {}
processOrder(
customerData: Customer,
items: Item[],
shippingAddress: Address,
payment: Payment,
shipping: ShippingCalculator,
discount: DiscountStrategy,
notification: NotificationService
): void {
this.validator.validate(customerData);
const subtotal = this.calculator.calculateTotal(items);
const total = discount.applyDiscount(subtotal);
const shippingCost = shipping.calculateShippingCost(shippingAddress);
payment.processPayment(total + shippingCost);
this.invoiceGenerator.generate(customerData, items, total, shippingCost);
notification.sendNotification("Order confirmed!");
this.repository.save({ customerData, items, total });
}
}
See the complete working solution with all implementations, tests, and examples:
👉 GitHub Repository
When NOT to Use SOLID
Don't over-engineer small projects:
- Prototypes/MVPs: Ship fast, refactor later
- Simple CRUD apps: Sometimes a God Class is fine for 3 endpoints
- Scripts/utilities: A 50-line script doesn't need dependency injection
Start simple. Refactor when you feel pain (tests are hard, changes break things, adding features takes too long).
Quick Reference
| Principle | Question | Red Flag | Quick Fix |
|---|---|---|---|
| SRP | Does this class have one reason to change? | Multiple responsibilities | Extract separate classes |
| OCP | Can I add features without modifying code? | Long if-else chains | Use strategy pattern or polymorphism |
| LSP | Can I swap implementations safely? | No abstractions | Define interfaces/base classes |
| ISP | Am I forced to depend on unused methods? | Fat interfaces | Split into smaller, focused interfaces |
| DIP | Do I depend on abstractions? | Direct instantiation | Inject dependencies |
Using AI to Write SOLID Code
AI can generate clean code—you just need to prompt it correctly. Here's how:
The Prompt
You are a senior software engineer who follows SOLID principles strictly.
When writing code:
- Each class should have ONE responsibility
- Use abstractions (interfaces/abstract classes) instead of concrete implementations
- Apply dependency injection via constructors
- Use strategy pattern for conditional logic (no long if-else chains)
- Keep classes small and focused
For the following requirement, provide:
1. Interface/abstract class definitions
2. Concrete implementations
3. A simple orchestrator class
4. Usage example
Requirement: [YOUR REQUIREMENT HERE]
Enforcing SOLID in Cursor/Windsurf
Create a .cursorrules or .windsurfrules file in your project root:
# Code Style Rules
## Architecture Principles
- Follow SOLID principles strictly
- Each class must have a single responsibility
- Use dependency injection via constructor
- Depend on abstractions, not concrete classes
- No God Classes - break down into focused classes
## Code Patterns
- Replace if-else chains with strategy pattern
- Use factory pattern for object creation
- Apply polymorphism over conditionals
- Keep methods under 20 lines
- Keep classes under 200 lines
## Testing Requirements
- All business logic must be testable
- Use dependency injection to enable mocking
- Write unit tests for each class independently
## Red Flags to Avoid
- Classes with 5+ methods doing unrelated things
- Methods with 3+ levels of nesting
- Direct instantiation of dependencies (use DI)
- Long parameter lists (>4 parameters suggests poor design)
In Cursor/Windsurf, reference this during prompts:
@.cursorrules Refactor this code following our SOLID principles
The AI will now automatically apply these constraints. No more excuses for messy code.
Key Takeaways
Before SOLID: One massive class doing everything. Adding Apple Pay means touching payment, validation, and database code.
After SOLID:
- Want Apple Pay? Create
ApplePayPaymentclass. Done. - Want SMS notifications? Create
SMSNotificationclass. Done. - Want to test payments? Mock just the
Paymentinterface. Done.
The goal isn't perfect architecture—it's code that adapts to change without breaking everything else.
Building an AI SaaS with clean architecture? I write about scalable patterns and practical refactoring. Follow me on Dev.to| GitHub
Top comments (0)