AiTechWorlds
AiTechWorlds
In 2013, a team at a major US healthcare insurer inherited a legacy billing system. The core function that calculated member premiums was 3,247 lines long. It had 14 nested if statements, 22 global variables, and had been modified by 31 different developers over 11 years — none of whom fully understood what it did.
Every change took weeks. Every fix introduced two new bugs. The system processed $2 billion in premiums annually and nobody dared rewrite it. This is the natural endpoint of code written without design principles.
The principles in this lesson exist as a direct response to this kind of nightmare.
Technical debt is a metaphor coined by Ward Cunningham in 1992. The idea: taking shortcuts to ship faster is like borrowing money. The debt accumulates interest — every future change becomes more expensive because of the shortcuts you took earlier.
Types of technical debt:
McKinsey research estimates that technical debt consumes 10–20% of technology budgets at large companies, and can account for up to 40% of the entire IT balance sheet at legacy-heavy enterprises.
The design principles below are how you avoid accumulating this debt in the first place.
Robert C. Martin ("Uncle Bob") formalised the SOLID principles in the early 2000s, drawing on decades of software engineering research. They are specifically for object-oriented design, but their spirit applies broadly.
A class should have only one reason to change.
Bad:
class UserService:
def create_user(self, name, email):
# Business logic
user = {"name": name, "email": email}
# Database logic
db.insert("users", user)
# Email logic
smtp.send(email, "Welcome!", "Thanks for joining")
return user
This class has THREE reasons to change: business rules, database schema, and email format. Change the email provider and you risk breaking user creation.
Good:
class UserService:
def __init__(self, user_repo, email_service):
self.repo = user_repo
self.email = email_service
def create_user(self, name, email):
user = User(name, email) # Business logic only
self.repo.save(user) # Delegate DB work
self.email.send_welcome(email) # Delegate email work
return user
Now UserService has one responsibility: orchestrating user creation. Database and email are separate concerns.
Classes should be open for extension, but closed for modification.
Adding new behaviour should not require editing existing, tested code.
Bad: Every new payment method requires editing the PaymentProcessor class.
Good:
from abc import ABC, abstractmethod
class PaymentMethod(ABC): # Abstract base — never modify
@abstractmethod
def charge(self, amount): ...
class CreditCard(PaymentMethod): # Extension — add without modifying base
def charge(self, amount):
print(f"Charging ${amount} to credit card")
class PayPal(PaymentMethod): # Another extension
def charge(self, amount):
print(f"Charging ${amount} via PayPal")
class PaymentProcessor:
def process(self, method: PaymentMethod, amount):
method.charge(amount) # Works with any PaymentMethod
Adding Bitcoin support means creating a Bitcoin(PaymentMethod) class — zero changes to existing code.
Subclasses must be substitutable for their parent class without breaking the program.
Named after Barbara Liskov (1987 Turing Award winner). If code works with a Bird, it must work with any subclass of Bird.
Violation:
class Bird:
def fly(self): print("Flying")
class Penguin(Bird):
def fly(self):
raise NotImplementedError("Penguins can't fly!") # VIOLATION
A Penguin is a Bird biologically, but breaks the fly() contract. Fix: restructure the hierarchy — FlyingBird and NonFlyingBird.
Clients should not be forced to depend on interfaces they do not use.
Bad: One fat interface forces all implementors to implement methods they do not need.
Good:
class Printable(ABC):
@abstractmethod
def print(self): ...
class Scannable(ABC):
@abstractmethod
def scan(self): ...
class SimplePrinter(Printable): # Only implements what it supports
def print(self): print("Printing...")
class AllInOne(Printable, Scannable): # Implements both
def print(self): print("Printing...")
def scan(self): print("Scanning...")
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Bad:
class OrderService:
def __init__(self):
self.db = MySQLDatabase() # Hard-coded dependency — can't swap or test
Good:
class OrderService:
def __init__(self, database: Database): # Depends on abstraction
self.db = database # Inject the concrete implementation
# In production:
service = OrderService(MySQLDatabase())
# In tests:
service = OrderService(InMemoryDatabase()) # Easy to test
Every piece of knowledge must have a single, unambiguous, authoritative representation in a system.
Coined by Andrew Hunt and David Thomas in "The Pragmatic Programmer" (1999).
Code duplication is not just ugly — it is a maintenance trap. When the same logic exists in three places, a bug fix in one place leaves two copies broken. A tax calculation copied across four services means four places to update when tax law changes.
The fix is not always "extract a function" — sometimes it is consolidating the knowledge (business rule, algorithm, configuration value) into a canonical place.
Attributed to Kelly Johnson of Lockheed's Skunk Works division (1960). The principle: the simplest solution that correctly solves the problem is the best solution.
"Simplicity is prerequisite for reliability." — Edsger Dijkstra
Signs you are violating KISS: six layers of abstraction for a problem that needs two; a design pattern where a function would do; a microservices architecture for a 3-person startup.
From Extreme Programming (Ron Jeffries, 1999): do not implement a feature until you actually need it.
Engineers love to anticipate future requirements. "We might need multi-currency support someday" becomes a complex, over-engineered abstraction layer that the product never actually needs. You paid the implementation cost, the testing cost, and the ongoing maintenance cost for zero business value.
Google's engineering culture explicitly warns against "speculative generality" — code written for hypothetical future use cases.
Talk only to your immediate friends. Do not reach through an object to talk to a stranger.
Violation:
# Reaches through 3 objects — breaks encapsulation
customer.getOrder().getShipping().getAddress().getCity()
Better:
customer.getShippingCity() # Customer knows how to answer this
The first form means CustomerService depends on the internal structure of Order, Shipping, and Address. Change any of them and CustomerService breaks.
Code smells are surface indicators of deeper design problems (Martin Fowler's "Refactoring," 1999):
| Smell | Description | Common Fix |
|---|---|---|
| Long Method | Method > 20 lines usually does too much | Extract Method |
| Large Class | Class with too many responsibilities | Extract Class |
| Magic Numbers | Literal values with no explanation (if age > 65) | Named constants (RETIREMENT_AGE = 65) |
| Dead Code | Unreachable or unused code | Delete it |
| Duplicate Code | Same logic in multiple places | Extract to shared function |
| God Object | One class that knows and does everything | Decompose by responsibility |
| Feature Envy | Method uses another class's data more than its own | Move Method |
Refactoring is improving internal structure without changing external behaviour. The safety net is a comprehensive test suite — refactor with confidence only when tests cover the code.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises