Back to Technology

System Design Series Part 12: Low-Level Design (LLD) Fundamentals

January 25, 2026 Wasil Zafar 50 min read

Master Low-Level Design (LLD) fundamentals including object-oriented design principles, SOLID, design patterns (Factory, Singleton, Observer), data modeling, schema design, and building maintainable, modular code architectures.

Table of Contents

  1. LLD vs HLD
  2. Object-Oriented Design
  3. SOLID Principles
  4. Design Patterns
  5. Data Modeling & Schema Design
  6. Code Modularity & Reusability
  7. Next Steps

Low-Level Design vs High-Level Design

Series Navigation: This is Part 12 of the 15-part System Design Series. Review Part 11: Real-World Case Studies first.

Low-Level Design (LLD) focuses on the internal workings of individual components—class diagrams, method signatures, data structures, and algorithms. While High-Level Design (HLD) answers "what components do we need?", LLD answers "how do we implement each component?"

Key Insight: Great system architects understand both HLD and LLD. HLD without LLD leads to architectures that can't be implemented; LLD without HLD leads to code that doesn't fit together.

HLD vs LLD Comparison

Aspect High-Level Design (HLD) Low-Level Design (LLD)
Focus System architecture, components Class design, methods, algorithms
Abstraction High (boxes and arrows) Low (code-level detail)
Audience Architects, product managers Developers, code reviewers
Artifacts Architecture diagrams, data flow Class diagrams, sequence diagrams
Questions "How do components interact?" "How is this class implemented?"

Object-Oriented Design Fundamentals

Object-Oriented Design (OOD) is the foundation of LLD. It focuses on organizing code into objects that contain both data and behavior. The four pillars of OOP are:

  • Encapsulation: Hide internal state, expose controlled interfaces
  • Abstraction: Focus on what objects do, not how they do it
  • Inheritance: Reuse code through hierarchical relationships
  • Polymorphism: Treat different types through common interfaces

Encapsulation & Abstraction

Encapsulation Example

# Bad: Exposed internal state
class BankAccount:
    def __init__(self):
        self.balance = 0  # Public - anyone can modify!

# Good: Encapsulated with controlled access
class BankAccount:
    def __init__(self, initial_balance=0):
        self._balance = initial_balance  # Protected
        self._transactions = []
    
    @property
    def balance(self):
        """Read-only access to balance"""
        return self._balance
    
    def deposit(self, amount):
        """Controlled mutation with validation"""
        if amount <= 0:
            raise ValueError("Deposit amount must be positive")
        self._balance += amount
        self._transactions.append(('deposit', amount))
    
    def withdraw(self, amount):
        """Controlled mutation with business logic"""
        if amount <= 0:
            raise ValueError("Withdrawal amount must be positive")
        if amount > self._balance:
            raise InsufficientFundsError()
        self._balance -= amount
        self._transactions.append(('withdraw', amount))
    
    def get_statement(self):
        """Controlled access to transaction history"""
        return list(self._transactions)  # Return copy, not reference

Abstraction Example

from abc import ABC, abstractmethod

# Abstract base class - defines WHAT, not HOW
class PaymentProcessor(ABC):
    @abstractmethod
    def process_payment(self, amount: float, currency: str) -> bool:
        """Process a payment. Implementation varies by provider."""
        pass
    
    @abstractmethod
    def refund(self, transaction_id: str) -> bool:
        """Refund a transaction."""
        pass

# Concrete implementations - define HOW
class StripeProcessor(PaymentProcessor):
    def __init__(self, api_key):
        self.stripe = StripeClient(api_key)
    
    def process_payment(self, amount, currency):
        return self.stripe.charges.create(
            amount=int(amount * 100),
            currency=currency
        )
    
    def refund(self, transaction_id):
        return self.stripe.refunds.create(charge=transaction_id)

class PayPalProcessor(PaymentProcessor):
    def __init__(self, client_id, secret):
        self.paypal = PayPalClient(client_id, secret)
    
    def process_payment(self, amount, currency):
        return self.paypal.create_payment(amount, currency)
    
    def refund(self, transaction_id):
        return self.paypal.refund_payment(transaction_id)

# Client code works with abstraction
def checkout(processor: PaymentProcessor, amount: float):
    """Works with ANY payment processor"""
    return processor.process_payment(amount, 'USD')

Inheritance & Polymorphism

Inheritance Hierarchy

# Base class
class Shape:
    def __init__(self, color):
        self.color = color
    
    def area(self):
        raise NotImplementedError("Subclasses must implement area()")
    
    def perimeter(self):
        raise NotImplementedError("Subclasses must implement perimeter()")
    
    def describe(self):
        return f"A {self.color} {self.__class__.__name__}"

# Derived classes
class Rectangle(Shape):
    def __init__(self, color, width, height):
        super().__init__(color)
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height
    
    def perimeter(self):
        return 2 * (self.width + self.height)

class Circle(Shape):
    def __init__(self, color, radius):
        super().__init__(color)
        self.radius = radius
    
    def area(self):
        return 3.14159 * self.radius ** 2
    
    def perimeter(self):
        return 2 * 3.14159 * self.radius

# Polymorphism: Same interface, different behavior
def total_area(shapes: list[Shape]) -> float:
    """Works with ANY shape - polymorphism in action"""
    return sum(shape.area() for shape in shapes)

# Usage
shapes = [
    Rectangle('red', 10, 5),
    Circle('blue', 7),
    Rectangle('green', 3, 3)
]
print(total_area(shapes))  # Works for all shapes

SOLID Principles

Key Insight: SOLID principles are the foundation of maintainable object-oriented code. They help create systems that are easy to extend, modify, and test.

S - Single Responsibility Principle

Definition: A class should have only one reason to change.

# Bad: Multiple responsibilities
class UserManager:
    def create_user(self, data): ...
    def send_welcome_email(self, user): ...  # Email responsibility
    def generate_report(self, users): ...     # Reporting responsibility
    def save_to_database(self, user): ...     # Persistence responsibility

# Good: Single responsibility per class
class UserService:
    def __init__(self, repository, email_service):
        self.repository = repository
        self.email_service = email_service
    
    def create_user(self, data):
        user = User(**data)
        self.repository.save(user)
        self.email_service.send_welcome(user)
        return user

class UserRepository:
    def save(self, user): ...
    def find_by_id(self, id): ...
    def find_all(self): ...

class EmailService:
    def send_welcome(self, user): ...
    def send_password_reset(self, user): ...

class ReportGenerator:
    def generate_user_report(self, users): ...

Open/Closed Principle

O - Open for Extension, Closed for Modification

Definition: Software entities should be open for extension but closed for modification.

# Bad: Must modify class to add new discount types
class DiscountCalculator:
    def calculate(self, order, discount_type):
        if discount_type == 'percentage':
            return order.total * 0.1
        elif discount_type == 'fixed':
            return 10.0
        elif discount_type == 'bogo':  # Adding new type requires modification
            return order.total * 0.5

# Good: Open for extension via new classes
from abc import ABC, abstractmethod

class DiscountStrategy(ABC):
    @abstractmethod
    def calculate(self, order) -> float:
        pass

class PercentageDiscount(DiscountStrategy):
    def __init__(self, percent):
        self.percent = percent
    
    def calculate(self, order):
        return order.total * (self.percent / 100)

class FixedDiscount(DiscountStrategy):
    def __init__(self, amount):
        self.amount = amount
    
    def calculate(self, order):
        return min(self.amount, order.total)

class BuyOneGetOneDiscount(DiscountStrategy):
    def calculate(self, order):
        return order.total * 0.5

# Adding new discount = new class, no modification needed
class LoyaltyDiscount(DiscountStrategy):
    def calculate(self, order):
        return order.total * (order.customer.loyalty_years * 0.02)

# Calculator is closed for modification
class DiscountCalculator:
    def apply(self, order, strategy: DiscountStrategy):
        return strategy.calculate(order)

Liskov Substitution Principle

L - Subtypes Must Be Substitutable

Definition: Objects of a superclass should be replaceable with objects of its subclasses without breaking the application.

# Bad: Square violates LSP for Rectangle
class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height
    
    def set_width(self, width):
        self._width = width
    
    def set_height(self, height):
        self._height = height
    
    def area(self):
        return self._width * self._height

class Square(Rectangle):  # Square IS-A Rectangle?
    def set_width(self, width):
        self._width = width
        self._height = width  # Violates expected behavior!
    
    def set_height(self, height):
        self._width = height
        self._height = height

# This breaks with Square:
def test_rectangle(rect: Rectangle):
    rect.set_width(5)
    rect.set_height(4)
    assert rect.area() == 20  # Fails for Square!

# Good: Separate hierarchy
class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height

class Square(Shape):  # Square is NOT a subtype of Rectangle
    def __init__(self, side):
        self.side = side
    
    def area(self):
        return self.side ** 2

Interface Segregation Principle

I - Prefer Many Specific Interfaces

Definition: Clients should not be forced to depend on interfaces they don't use.

# Bad: Fat interface
class Worker(ABC):
    @abstractmethod
    def work(self): pass
    
    @abstractmethod
    def eat(self): pass
    
    @abstractmethod
    def sleep(self): pass

class Robot(Worker):
    def work(self):
        print("Working...")
    
    def eat(self):
        pass  # Robots don't eat! Forced to implement.
    
    def sleep(self):
        pass  # Robots don't sleep! Forced to implement.

# Good: Segregated interfaces
class Workable(ABC):
    @abstractmethod
    def work(self): pass

class Eatable(ABC):
    @abstractmethod
    def eat(self): pass

class Sleepable(ABC):
    @abstractmethod
    def sleep(self): pass

class Human(Workable, Eatable, Sleepable):
    def work(self):
        print("Human working...")
    
    def eat(self):
        print("Human eating...")
    
    def sleep(self):
        print("Human sleeping...")

class Robot(Workable):  # Only implements what it needs
    def work(self):
        print("Robot working...")

Dependency Inversion Principle

D - Depend on Abstractions, Not Concretions

Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.

# Bad: High-level depends on low-level
class MySQLDatabase:
    def save(self, data):
        print(f"Saving to MySQL: {data}")

class UserService:
    def __init__(self):
        self.db = MySQLDatabase()  # Tight coupling!
    
    def create_user(self, data):
        self.db.save(data)  # Locked to MySQL

# Good: Both depend on abstraction
class Database(ABC):
    @abstractmethod
    def save(self, data): pass
    
    @abstractmethod
    def find(self, id): pass

class MySQLDatabase(Database):
    def save(self, data):
        print(f"Saving to MySQL: {data}")
    
    def find(self, id):
        return {"id": id, "source": "mysql"}

class MongoDatabase(Database):
    def save(self, data):
        print(f"Saving to MongoDB: {data}")
    
    def find(self, id):
        return {"id": id, "source": "mongodb"}

class UserService:
    def __init__(self, db: Database):  # Depends on abstraction
        self.db = db
    
    def create_user(self, data):
        self.db.save(data)

# Flexible - can swap implementations
mysql_service = UserService(MySQLDatabase())
mongo_service = UserService(MongoDatabase())

Design Patterns

Design patterns are reusable solutions to common software design problems. They fall into three categories:

  • Creational: Object creation mechanisms
  • Structural: Object composition and relationships
  • Behavioral: Object communication and responsibility

Creational Patterns

Factory Pattern

Encapsulates object creation logic, allowing the client to request objects without knowing the concrete class.

# Factory Pattern
class NotificationFactory:
    @staticmethod
    def create(notification_type: str, message: str):
        if notification_type == 'email':
            return EmailNotification(message)
        elif notification_type == 'sms':
            return SMSNotification(message)
        elif notification_type == 'push':
            return PushNotification(message)
        else:
            raise ValueError(f"Unknown type: {notification_type}")

# Usage - client doesn't know concrete classes
notification = NotificationFactory.create('email', 'Hello!')
notification.send()

Singleton Pattern

Ensures a class has only one instance and provides global access to it.

# Thread-safe Singleton
import threading

class DatabaseConnection:
    _instance = None
    _lock = threading.Lock()
    
    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
                    cls._instance._initialize()
        return cls._instance
    
    def _initialize(self):
        self.connection = self._create_connection()
    
    def _create_connection(self):
        return "Connected to database"

# Usage - always same instance
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2)  # True

Builder Pattern

Separates construction of complex objects from their representation.

# Builder Pattern
class QueryBuilder:
    def __init__(self):
        self._select = []
        self._from = None
        self._where = []
        self._order_by = []
        self._limit = None
    
    def select(self, *columns):
        self._select.extend(columns)
        return self
    
    def from_table(self, table):
        self._from = table
        return self
    
    def where(self, condition):
        self._where.append(condition)
        return self
    
    def order_by(self, column, direction='ASC'):
        self._order_by.append(f"{column} {direction}")
        return self
    
    def limit(self, count):
        self._limit = count
        return self
    
    def build(self):
        query = f"SELECT {', '.join(self._select) or '*'} FROM {self._from}"
        if self._where:
            query += f" WHERE {' AND '.join(self._where)}"
        if self._order_by:
            query += f" ORDER BY {', '.join(self._order_by)}"
        if self._limit:
            query += f" LIMIT {self._limit}"
        return query

# Fluent interface
query = (QueryBuilder()
    .select('name', 'email', 'created_at')
    .from_table('users')
    .where('status = "active"')
    .where('age > 18')
    .order_by('created_at', 'DESC')
    .limit(10)
    .build())

print(query)
# SELECT name, email, created_at FROM users WHERE status = "active" AND age > 18 ORDER BY created_at DESC LIMIT 10

Structural Patterns

Adapter Pattern

Allows incompatible interfaces to work together by wrapping an object with a compatible interface.

# Adapter Pattern
# Legacy payment system
class LegacyPaymentGateway:
    def make_payment(self, account_number, amount_in_cents):
        print(f"Legacy: Charging {amount_in_cents} cents to {account_number}")
        return True

# Modern interface we want to use
class PaymentProcessor(ABC):
    @abstractmethod
    def charge(self, card: dict, amount: float) -> bool:
        pass

# Adapter bridges the gap
class LegacyPaymentAdapter(PaymentProcessor):
    def __init__(self, legacy_gateway: LegacyPaymentGateway):
        self.legacy = legacy_gateway
    
    def charge(self, card: dict, amount: float) -> bool:
        # Convert modern call to legacy format
        account = card['number'].replace('-', '')
        cents = int(amount * 100)
        return self.legacy.make_payment(account, cents)

# Usage - modern interface, legacy implementation
adapter = LegacyPaymentAdapter(LegacyPaymentGateway())
adapter.charge({'number': '1234-5678-9012-3456'}, 99.99)

Decorator Pattern

Dynamically adds behavior to objects without modifying their class.

# Decorator Pattern
class Coffee(ABC):
    @abstractmethod
    def cost(self) -> float:
        pass
    
    @abstractmethod
    def description(self) -> str:
        pass

class SimpleCoffee(Coffee):
    def cost(self):
        return 2.00
    
    def description(self):
        return "Simple coffee"

class CoffeeDecorator(Coffee):
    def __init__(self, coffee: Coffee):
        self._coffee = coffee

class MilkDecorator(CoffeeDecorator):
    def cost(self):
        return self._coffee.cost() + 0.50
    
    def description(self):
        return self._coffee.description() + ", milk"

class SugarDecorator(CoffeeDecorator):
    def cost(self):
        return self._coffee.cost() + 0.25
    
    def description(self):
        return self._coffee.description() + ", sugar"

class WhippedCreamDecorator(CoffeeDecorator):
    def cost(self):
        return self._coffee.cost() + 0.75
    
    def description(self):
        return self._coffee.description() + ", whipped cream"

# Build complex object by stacking decorators
coffee = SimpleCoffee()
coffee = MilkDecorator(coffee)
coffee = SugarDecorator(coffee)
coffee = WhippedCreamDecorator(coffee)

print(coffee.description())  # "Simple coffee, milk, sugar, whipped cream"
print(f"${coffee.cost():.2f}")  # "$3.50"

Facade Pattern

Provides a simplified interface to a complex subsystem.

# Facade Pattern
# Complex subsystems
class VideoDecoder:
    def decode(self, file): return f"Decoded: {file}"

class AudioDecoder:
    def decode(self, file): return f"Audio extracted: {file}"

class SubtitleLoader:
    def load(self, file): return f"Subtitles loaded: {file}"

class VideoPlayer:
    def play(self, video, audio, subtitles):
        print(f"Playing: {video} with {audio} and {subtitles}")

# Facade - simple interface
class MediaPlayerFacade:
    def __init__(self):
        self.video_decoder = VideoDecoder()
        self.audio_decoder = AudioDecoder()
        self.subtitle_loader = SubtitleLoader()
        self.player = VideoPlayer()
    
    def play_movie(self, filename):
        """Simple interface to complex subsystem"""
        video = self.video_decoder.decode(filename)
        audio = self.audio_decoder.decode(filename)
        subtitles = self.subtitle_loader.load(filename)
        self.player.play(video, audio, subtitles)

# Usage - client only knows facade
player = MediaPlayerFacade()
player.play_movie("movie.mp4")

Behavioral Patterns

Observer Pattern

Defines a one-to-many dependency so that when one object changes state, all dependents are notified.

# Observer Pattern
class EventEmitter:
    def __init__(self):
        self._observers = {}
    
    def subscribe(self, event: str, callback):
        if event not in self._observers:
            self._observers[event] = []
        self._observers[event].append(callback)
    
    def unsubscribe(self, event: str, callback):
        if event in self._observers:
            self._observers[event].remove(callback)
    
    def emit(self, event: str, data=None):
        if event in self._observers:
            for callback in self._observers[event]:
                callback(data)

# Usage
class UserService(EventEmitter):
    def create_user(self, data):
        user = User(**data)
        self.emit('user_created', user)
        return user

# Subscribe to events
service = UserService()
service.subscribe('user_created', lambda u: print(f"Sending welcome email to {u.email}"))
service.subscribe('user_created', lambda u: print(f"Logging: User {u.id} created"))
service.subscribe('user_created', lambda u: print(f"Updating analytics..."))

service.create_user({'name': 'Alice', 'email': 'alice@example.com'})

Strategy Pattern

Defines a family of algorithms, encapsulates each one, and makes them interchangeable.

# Strategy Pattern
class CompressionStrategy(ABC):
    @abstractmethod
    def compress(self, data: bytes) -> bytes:
        pass

class ZipCompression(CompressionStrategy):
    def compress(self, data):
        import zlib
        return zlib.compress(data)

class GzipCompression(CompressionStrategy):
    def compress(self, data):
        import gzip
        return gzip.compress(data)

class NoCompression(CompressionStrategy):
    def compress(self, data):
        return data

class FileUploader:
    def __init__(self, compression: CompressionStrategy):
        self.compression = compression
    
    def upload(self, data: bytes, filename: str):
        compressed = self.compression.compress(data)
        print(f"Uploading {filename}: {len(data)} ? {len(compressed)} bytes")
        # Upload logic...

# Switch strategies at runtime
uploader = FileUploader(ZipCompression())
uploader.upload(b"x" * 1000, "file.txt")

uploader.compression = GzipCompression()  # Change strategy
uploader.upload(b"x" * 1000, "file2.txt")

Command Pattern

Encapsulates a request as an object, allowing parameterization, queuing, and undo operations.

# Command Pattern
class Command(ABC):
    @abstractmethod
    def execute(self): pass
    
    @abstractmethod
    def undo(self): pass

class TextEditor:
    def __init__(self):
        self.content = ""
    
    def insert(self, text, position):
        self.content = self.content[:position] + text + self.content[position:]
    
    def delete(self, start, end):
        self.content = self.content[:start] + self.content[end:]

class InsertCommand(Command):
    def __init__(self, editor: TextEditor, text: str, position: int):
        self.editor = editor
        self.text = text
        self.position = position
    
    def execute(self):
        self.editor.insert(self.text, self.position)
    
    def undo(self):
        self.editor.delete(self.position, self.position + len(self.text))

class CommandHistory:
    def __init__(self):
        self.history = []
    
    def execute(self, command: Command):
        command.execute()
        self.history.append(command)
    
    def undo(self):
        if self.history:
            command = self.history.pop()
            command.undo()

# Usage with undo support
editor = TextEditor()
history = CommandHistory()

history.execute(InsertCommand(editor, "Hello", 0))
print(editor.content)  # "Hello"

history.execute(InsertCommand(editor, " World", 5))
print(editor.content)  # "Hello World"

history.undo()
print(editor.content)  # "Hello"

Data Modeling & Schema Design

Good data modeling is essential for LLD. It determines how data is stored, accessed, and maintained. Key considerations:

  • Entity relationships: One-to-one, one-to-many, many-to-many
  • Data integrity: Constraints, foreign keys, cascades
  • Query patterns: Design schema around common queries
  • Scalability: Plan for data growth and access patterns

Normalization vs Denormalization

Aspect Normalized Denormalized
Data duplication Minimal Intentional redundancy
Write performance Fast (single update) Slower (multiple updates)
Read performance Requires JOINs Fast (no JOINs)
Data consistency Easy to maintain Must sync duplicates
Use case OLTP, write-heavy OLAP, read-heavy

Example: E-commerce Schema

-- Normalized Schema (3NF)
CREATE TABLE customers (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    name VARCHAR(100) NOT NULL
);

CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    customer_id INT REFERENCES customers(id),
    created_at TIMESTAMP DEFAULT NOW(),
    status VARCHAR(20) DEFAULT 'pending'
);

CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    price DECIMAL(10, 2) NOT NULL
);

CREATE TABLE order_items (
    order_id INT REFERENCES orders(id),
    product_id INT REFERENCES products(id),
    quantity INT NOT NULL,
    unit_price DECIMAL(10, 2) NOT NULL,  -- Denormalized: price at time of order
    PRIMARY KEY (order_id, product_id)
);

-- Denormalized for reporting (materialized view)
CREATE MATERIALIZED VIEW order_summary AS
SELECT 
    o.id AS order_id,
    c.name AS customer_name,
    c.email AS customer_email,
    o.created_at,
    o.status,
    SUM(oi.quantity * oi.unit_price) AS total_amount,
    COUNT(oi.product_id) AS item_count
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN order_items oi ON o.id = oi.order_id
GROUP BY o.id, c.name, c.email, o.created_at, o.status;

Indexing Strategies

Index Types and When to Use Them

-- B-Tree Index (default) - Equality and range queries
CREATE INDEX idx_orders_customer ON orders(customer_id);
CREATE INDEX idx_orders_created ON orders(created_at);

-- Composite Index - Multi-column queries
-- Order matters! (customer_id, status) supports:
--   WHERE customer_id = ?
--   WHERE customer_id = ? AND status = ?
-- But NOT: WHERE status = ?
CREATE INDEX idx_orders_cust_status ON orders(customer_id, status);

-- Partial Index - Index subset of rows
CREATE INDEX idx_pending_orders ON orders(created_at)
WHERE status = 'pending';

-- Expression Index - Index computed values
CREATE INDEX idx_orders_date ON orders(DATE(created_at));

-- Full-text Index - Text search
CREATE INDEX idx_products_search ON products 
USING gin(to_tsvector('english', name || ' ' || description));

-- Hash Index - Equality only (no range)
CREATE INDEX idx_users_email ON users USING hash(email);
Index Guidelines: Index columns in WHERE, JOIN, ORDER BY clauses. Avoid over-indexing (slows writes). Monitor query plans with EXPLAIN ANALYZE.

Code Modularity & Reusability

Layered Architecture

# Clean Architecture / Layered Structure
"""
project/
+-- domain/              # Core business logic (no dependencies)
¦   +-- entities/
¦   ¦   +-- user.py
¦   +-- repositories/   # Interfaces only
¦   ¦   +-- user_repository.py
¦   +-- services/
¦       +-- user_service.py
+-- infrastructure/      # External integrations
¦   +-- database/
¦   ¦   +-- postgres_user_repository.py
¦   +-- external/
¦       +-- email_client.py
+-- application/         # Use cases, orchestration
¦   +-- use_cases/
¦       +-- create_user.py
+-- presentation/        # API, CLI, Web
    +-- api/
        +-- user_routes.py
"""

# Domain Layer - Pure business logic
class User:
    def __init__(self, email: str, name: str):
        self.email = email
        self.name = name
        self.validate()
    
    def validate(self):
        if '@' not in self.email:
            raise ValueError("Invalid email")

class UserRepository(ABC):
    @abstractmethod
    def save(self, user: User) -> User: pass
    
    @abstractmethod
    def find_by_email(self, email: str) -> User | None: pass

# Infrastructure Layer - Concrete implementations
class PostgresUserRepository(UserRepository):
    def __init__(self, connection):
        self.conn = connection
    
    def save(self, user: User) -> User:
        self.conn.execute(
            "INSERT INTO users (email, name) VALUES (%s, %s)",
            (user.email, user.name)
        )
        return user

# Application Layer - Use cases
class CreateUserUseCase:
    def __init__(self, repo: UserRepository, email_service: EmailService):
        self.repo = repo
        self.email_service = email_service
    
    def execute(self, email: str, name: str) -> User:
        existing = self.repo.find_by_email(email)
        if existing:
            raise UserAlreadyExistsError()
        
        user = User(email, name)
        self.repo.save(user)
        self.email_service.send_welcome(user)
        return user
Dependency Rule: Dependencies point inward. Domain has no dependencies. Infrastructure depends on domain. Application orchestrates both.

Next Steps

You now have a strong foundation in Low-Level Design! Continue to Part 13 to explore Distributed Systems Deep Dive, covering consensus algorithms, distributed coordination, and distributed file systems.

Low-Level Design (LLD) Document Generator

Document your class design, design patterns, data models, and API contracts at the component level. Download as Word, Excel, or PDF.

Draft auto-saved

All data stays in your browser. Nothing is sent to or stored on any server.

Technology