DevNextGen DOCS

Developer Documentation

Comprehensive guides on software engineering principles, design patterns, architectural patterns, and best practices for building production-ready applications.

SOLID Principles

SOLID is an acronym for five design principles that help developers create maintainable, flexible, and scalable software. Introduced by Robert C. Martin, these principles form the foundation of good object-oriented design.

S Single Responsibility Principle

A class should have only one reason to change. Each module or class should be responsible for a single part of the functionality.

# Bad: One class handles both user data and email
class UserManager:
    def create_user(self, data):
        # saves user to database
        ...
    def send_welcome_email(self, user):
        # sends email — different responsibility
        ...

# Good: Separated responsibilities
class UserRepository:
    def create(self, data): ...

class EmailService:
    def send_welcome(self, user): ...
O Open/Closed Principle

Software entities should be open for extension but closed for modification. Add new behavior without changing existing code.

# Bad: Modify existing code for new payment types
class PaymentProcessor:
    def process(self, payment_type, amount):
        if payment_type == "credit": ...
        elif payment_type == "debit": ...
        # Adding PayPal requires modifying this class

# Good: Extend via new classes
class PaymentHandler(Protocol):
    def process(self, amount: Decimal) -> bool: ...

class CreditCardHandler:
    def process(self, amount): ...

class PayPalHandler:  # New — no existing code changed
    def process(self, amount): ...
L Liskov Substitution Principle

Objects of a superclass should be replaceable with objects of a subclass without breaking the application. Subtypes must honor the contract of their base type.

# Bad: Square violates Rectangle's expected behavior
class Rectangle:
    def set_width(self, w): self.width = w
    def set_height(self, h): self.height = h

class Square(Rectangle):
    def set_width(self, w):
        self.width = self.height = w  # Breaks expectations

# Good: Use a shared interface instead
class Shape(Protocol):
    def area(self) -> float: ...
I Interface Segregation Principle

Clients should not be forced to depend on interfaces they don't use. Prefer many small, specific interfaces over one large general-purpose one.

# Bad: Forces all workers to implement all methods
class Worker(Protocol):
    def code(self): ...
    def design(self): ...
    def test(self): ...

# Good: Separate interfaces
class Coder(Protocol):
    def code(self): ...

class Designer(Protocol):
    def design(self): ...

class Tester(Protocol):
    def test(self): ...
D Dependency Inversion Principle

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details.

# Bad: High-level depends directly on low-level
class OrderService:
    def __init__(self):
        self.db = MySQLDatabase()  # Tightly coupled

# Good: Depend on abstraction
class Database(Protocol):
    def save(self, entity): ...
    def find(self, id): ...

class OrderService:
    def __init__(self, db: Database):
        self.db = db  # Injected — easy to swap

DRY, KISS, YAGNI

DRY — Don't Repeat Yourself

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system. Duplicated logic leads to inconsistencies and maintenance burden.

Tip DRY is about knowledge duplication, not code duplication. Two functions that look identical but represent different business concepts should stay separate. Three similar lines of code are better than a premature abstraction.

KISS — Keep It Simple, Stupid

Most systems work best when kept simple. Avoid unnecessary complexity. If a straightforward solution solves the problem, prefer it over a clever one. Clever code is harder to debug, review, and maintain.

YAGNI — You Aren't Gonna Need It

Don't build features until they are actually needed. Speculative abstractions and "future-proofing" often create complexity without value. Implement for today's requirements; refactor when needs change.

# YAGNI violation: Building a plugin system for one use case
class PluginManager:
    def register(self, plugin): ...
    def discover(self): ...
    def load_all(self): ...
    # ...50 lines of framework code for one feature

# Better: Just write the feature directly
def process_data(data):
    return transform(data)  # Simple, direct

Clean Code Practices

Clean code is code that is easy to read, understand, and modify. It follows consistent conventions and communicates intent clearly.

Naming Conventions

  • Variables: Use descriptive names that reveal intent — elapsed_time_in_days not d
  • Functions: Verb phrases describing what they do — calculate_tax() not tax()
  • Classes: Noun phrases — UserRepository not ManageUsers
  • Booleans: Read as questions — is_active, has_permission, can_edit
  • Constants: UPPER_SNAKE_CASE — MAX_RETRY_COUNT, DEFAULT_TIMEOUT

Function Design

  • Small: Functions should do one thing. If you need to describe it with "and", split it.
  • Few arguments: Aim for 0-2 parameters. More than 3 signals the function does too much or needs a data class.
  • No side effects: A function named check_password() shouldn't also initialize a session.
  • Command-Query Separation: Functions should either change state or return data, not both.

Comments

# Bad: Restating the code
i += 1  # increment i by 1

# Bad: Commented-out code — use version control instead
# old_value = calculate_v1(data)

# Good: Explain WHY, not WHAT
# We retry 3 times because the upstream API has transient 503s
# during deployment windows (confirmed with their SRE team).
for attempt in range(3):
    ...

Creational Patterns

Creational patterns abstract the instantiation process, making systems independent of how objects are created, composed, and represented.

Singleton

Ensures a class has only one instance and provides a global point of access. Use sparingly — often a sign of hidden global state.

class DatabaseConnection:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._init_connection()
        return cls._instance

# Better alternative: Module-level instance
# Python modules are singletons by nature
_pool = create_connection_pool()
def get_pool():
    return _pool

Factory Method

Define an interface for creating objects, but let subclasses decide which class to instantiate. Useful when the exact type depends on runtime data.

class NotificationFactory:
    _registry: dict[str, type] = {}

    @classmethod
    def register(cls, channel: str, handler: type):
        cls._registry[channel] = handler

    @classmethod
    def create(cls, channel: str, **kwargs):
        handler = cls._registry.get(channel)
        if not handler:
            raise ValueError(f"Unknown channel: {channel}")
        return handler(**kwargs)

# Register handlers
NotificationFactory.register("email", EmailNotifier)
NotificationFactory.register("sms", SMSNotifier)
NotificationFactory.register("slack", SlackNotifier)

# Usage
notifier = NotificationFactory.create("email", to="user@example.com")

Builder

Separate the construction of a complex object from its representation. Useful for objects with many optional parameters.

# Using dataclass with defaults (Pythonic builder)
@dataclass
class QueryBuilder:
    table: str
    conditions: list[str] = field(default_factory=list)
    order_by: str | None = None
    limit: int | None = None

    def where(self, condition: str) -> "QueryBuilder":
        self.conditions.append(condition)
        return self

    def build(self) -> str:
        sql = f"SELECT * FROM {self.table}"
        if self.conditions:
            sql += " WHERE " + " AND ".join(self.conditions)
        if self.order_by:
            sql += f" ORDER BY {self.order_by}"
        if self.limit:
            sql += f" LIMIT {self.limit}"
        return sql

query = (QueryBuilder("users")
    .where("active = true")
    .where("age > 18")
    .build())

Structural Patterns

Structural patterns compose classes and objects to form larger structures while keeping them flexible and efficient.

Adapter

Convert the interface of a class into another interface clients expect. Useful when integrating third-party libraries or legacy code.

# Third-party payment library with different interface
class StripeClient:
    def create_charge(self, cents: int, token: str): ...

# Our interface
class PaymentGateway(Protocol):
    def charge(self, amount: Decimal, card_id: str) -> bool: ...

# Adapter
class StripeAdapter:
    def __init__(self, client: StripeClient):
        self._client = client

    def charge(self, amount: Decimal, card_id: str) -> bool:
        cents = int(amount * 100)
        return self._client.create_charge(cents, card_id)

Decorator

Attach additional responsibilities to an object dynamically. Python's decorator syntax (@decorator) is a language-level implementation of this pattern.

import functools, time, logging

def retry(max_attempts: int = 3, delay: float = 1.0):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts - 1:
                        raise
                    logging.warning(f"Attempt {attempt+1} failed: {e}")
                    time.sleep(delay * (2 ** attempt))
        return wrapper
    return decorator

@retry(max_attempts=3)
def fetch_data(url: str):
    ...

Repository

Mediates between the domain and data mapping layers, acting like an in-memory collection of domain objects.

class UserRepository(Protocol):
    async def get_by_id(self, user_id: str) -> User | None: ...
    async def save(self, user: User) -> None: ...
    async def find_by_email(self, email: str) -> User | None: ...

class PostgresUserRepository:
    def __init__(self, pool: asyncpg.Pool):
        self._pool = pool

    async def get_by_id(self, user_id: str) -> User | None:
        async with self._pool.acquire() as conn:
            row = await conn.fetchrow(
                "SELECT * FROM users WHERE id = $1", user_id
            )
            return User(**dict(row)) if row else None

Behavioral Patterns

Behavioral patterns define communication between objects, focusing on how they interact and distribute responsibility.

Strategy

Define a family of algorithms, encapsulate each one, and make them interchangeable. The strategy pattern lets the algorithm vary independently from clients that use it.

from typing import Protocol

class CompressionStrategy(Protocol):
    def compress(self, data: bytes) -> bytes: ...

class GzipCompression:
    def compress(self, data: bytes) -> bytes:
        import gzip
        return gzip.compress(data)

class LZ4Compression:
    def compress(self, data: bytes) -> bytes:
        import lz4.frame
        return lz4.frame.compress(data)

class DataExporter:
    def __init__(self, compression: CompressionStrategy):
        self._compression = compression

    def export(self, data: bytes, path: str):
        compressed = self._compression.compress(data)
        Path(path).write_bytes(compressed)

Observer / Event System

Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified automatically.

from collections import defaultdict
from typing import Callable

class EventBus:
    def __init__(self):
        self._handlers: dict[str, list[Callable]] = defaultdict(list)

    def subscribe(self, event: str, handler: Callable):
        self._handlers[event].append(handler)

    def publish(self, event: str, **data):
        for handler in self._handlers[event]:
            handler(**data)

# Usage
bus = EventBus()
bus.subscribe("user.created", send_welcome_email)
bus.subscribe("user.created", create_default_settings)
bus.publish("user.created", user_id="123", email="a@b.com")

Pattern Summary

PatternCategoryUse When
SingletonCreationalExactly one instance needed (DB pool, config)
FactoryCreationalObject type determined at runtime
BuilderCreationalComplex object with many optional params
AdapterStructuralIntegrating incompatible interfaces
DecoratorStructuralAdding behavior without subclassing
RepositoryStructuralAbstracting data access layer
StrategyBehavioralSwappable algorithms at runtime
ObserverBehavioralDecoupled event notification
IteratorBehavioralSequential access without exposing internals
CommandBehavioralEncapsulate requests as objects (undo/redo)

Architectural Patterns

Architectural patterns provide high-level strategies for organizing code at the system level. They define the overall structure of applications and how components interact.

Layered Architecture

The most common pattern. Organizes code into horizontal layers where each layer only depends on the layer directly below it.

┌─────────────────────────────────┐
│   Presentation Layer (UI/API)   │  ← Controllers, Views, Serializers
├─────────────────────────────────┤
│   Application Layer (Services)  │  ← Use cases, orchestration
├─────────────────────────────────┤
│   Domain Layer (Business Logic) │  ← Entities, value objects, rules
├─────────────────────────────────┤
│   Infrastructure Layer (Data)   │  ← DB, APIs, file system, cache
└─────────────────────────────────┘

Clean Architecture

Robert C. Martin's Clean Architecture emphasizes separation of concerns through concentric circles. Dependencies always point inward — outer layers depend on inner layers, never the reverse.

       ┌──────────────────────────────┐
       │  Frameworks & Drivers        │  FastAPI, PostgreSQL, Redis
       │  ┌────────────────────────┐  │
       │  │  Interface Adapters     │  │  Controllers, Gateways, Repos
       │  │  ┌──────────────────┐  │  │
       │  │  │  Use Cases       │  │  │  Application business rules
       │  │  │  ┌────────────┐  │  │  │
       │  │  │  │  Entities   │  │  │  │  Enterprise business rules
       │  │  │  └────────────┘  │  │  │
       │  │  └──────────────────┘  │  │
       │  └────────────────────────┘  │
       └──────────────────────────────┘
       Dependencies always point INWARD →

Key rules:

  • Entities know nothing about use cases, frameworks, or databases
  • Use cases orchestrate entities but don't know about HTTP or SQL
  • Interface adapters translate between use cases and external frameworks
  • Frameworks are plug-and-play — swapping PostgreSQL for MongoDB should only affect the outermost layer

Microservices Architecture

Decompose an application into small, independently deployable services. Each service owns its data, runs in its own process, and communicates via well-defined APIs.

When to use Microservices Start with a monolith. Only extract services when you have a clear need: independent scaling, different tech stacks, separate team ownership, or different deployment cadences. Premature decomposition creates distributed monolith problems.

Key Principles

  • Single Responsibility: Each service owns one bounded context
  • Database per Service: No shared databases — services communicate via APIs
  • Resilience: Services must handle failures in other services gracefully (circuit breaker, retry, fallback)
  • Observability: Distributed tracing, structured logging, metrics — you can't debug what you can't see
  • API Gateway: Single entry point for clients, handles routing, auth, rate limiting

Event-Driven Architecture

Components communicate through events rather than direct calls. Producers publish events; consumers react to them. Enables loose coupling and scalability.

# Event-driven with message broker
# Producer
async def place_order(order_data):
    order = Order.create(order_data)
    await db.save(order)
    await broker.publish("order.placed", {
        "order_id": order.id,
        "customer_id": order.customer_id,
        "total": order.total,
    })

# Consumer 1: Send confirmation email
@broker.subscribe("order.placed")
async def send_confirmation(event):
    await email.send(event["customer_id"], "Order confirmed!")

# Consumer 2: Update inventory
@broker.subscribe("order.placed")
async def update_inventory(event):
    await inventory.reserve(event["order_id"])

CQRS — Command Query Responsibility Segregation

Separate read and write models. Commands mutate state; queries read it. Often combined with Event Sourcing for complex domains.

  • Command side: Validates, processes business logic, persists events
  • Query side: Optimized read models (denormalized, cached, pre-computed)
  • Sync: Events from command side update the query-side projections

API Design Guidelines

RESTful Conventions

MethodPathActionStatus
GET/usersList users200
POST/usersCreate user201
GET/users/{id}Get user200
PUT/users/{id}Replace user200
PATCH/users/{id}Update fields200
DELETE/users/{id}Delete user204

Naming Rules

  • Use nouns, not verbs: /users not /getUsers
  • Use plural resource names: /orders not /order
  • Use kebab-case for multi-word: /order-items
  • Nest for relationships: /users/{id}/orders (max 2 levels deep)
  • Use query params for filtering: /users?role=admin&active=true

Pagination

# Cursor-based (preferred for large datasets)
GET /api/v1/items?cursor=eyJpZCI6MTAwfQ&limit=20

# Response
{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJpZCI6MTIwfQ",
    "has_more": true
  }
}

# Offset-based (simpler, less efficient for deep pages)
GET /api/v1/items?page=3&per_page=20

Error Responses

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Email is required",
    "details": [
      {
        "field": "email",
        "message": "This field is required",
        "code": "required"
      }
    ]
  }
}

Versioning

  • URL path (most common): /api/v1/users, /api/v2/users
  • Header: Accept: application/vnd.api+json;version=2
  • Don't break existing clients — add fields, don't rename or remove them

Error Handling

Principles

  • Fail fast: Validate inputs at system boundaries; don't pass bad data deeper into the system
  • Be specific: Catch specific exceptions, not bare except:
  • Don't swallow errors: An empty except: pass hides bugs
  • Log context: Include what operation was attempted, with what input, and what went wrong
  • Use custom exceptions: Domain-specific errors are more meaningful than generic ones
# Exception hierarchy for a domain
class AppError(Exception):
    def __init__(self, message: str, code: str):
        self.message = message
        self.code = code

class NotFoundError(AppError):
    def __init__(self, resource: str, id: str):
        super().__init__(f"{resource} {id} not found", "NOT_FOUND")

class ValidationError(AppError):
    def __init__(self, field: str, reason: str):
        super().__init__(f"{field}: {reason}", "VALIDATION_ERROR")

# FastAPI exception handler
@app.exception_handler(AppError)
async def handle_app_error(request, exc: AppError):
    status_map = {"NOT_FOUND": 404, "VALIDATION_ERROR": 422}
    return JSONResponse(
        status_code=status_map.get(exc.code, 500),
        content={"error": {"code": exc.code, "message": exc.message}},
    )

Testing Strategies

The Testing Pyramid

         /\          E2E Tests (few, slow, expensive)
        /  \         Test critical user journeys
       /    \
      /──────\       Integration Tests (moderate)
     / Service \      Test component interactions, APIs, DB
    /──────────\
   /   Unit     \    Unit Tests (many, fast, cheap)
  / ──────────── \   Test individual functions, classes
 /________________\

Unit Testing Best Practices

  • AAA Pattern: Arrange → Act → Assert. One logical assertion per test.
  • Test behavior, not implementation: Test what the function does, not how it does it.
  • Descriptive names: test_expired_token_returns_401 not test_auth_3
  • Independent tests: No test should depend on another test's state or ordering.
  • Fast: If unit tests take more than a few seconds, something is wrong.
# Good test structure
def test_discount_applied_when_order_exceeds_threshold():
    # Arrange
    order = Order(items=[Item(price=150.00)])
    discount = PercentageDiscount(threshold=100, percent=10)

    # Act
    total = discount.apply(order)

    # Assert
    assert total == 135.00

def test_no_discount_when_below_threshold():
    order = Order(items=[Item(price=50.00)])
    discount = PercentageDiscount(threshold=100, percent=10)

    total = discount.apply(order)

    assert total == 50.00

Integration Testing

Rule of Thumb Mock at the boundary, not in the middle. Test your code against real databases and APIs when possible. Mocks that diverge from real behavior give false confidence.
# Integration test with real database (pytest + testcontainers)
import pytest
from httpx import AsyncClient

@pytest.fixture
async def client(app, db):
    async with AsyncClient(app=app, base_url="http://test") as c:
        yield c

async def test_create_and_retrieve_user(client):
    # Create
    resp = await client.post("/users", json={"name": "Alice"})
    assert resp.status_code == 201
    user_id = resp.json()["id"]

    # Retrieve
    resp = await client.get(f"/users/{user_id}")
    assert resp.json()["name"] == "Alice"

Git Workflow

Branch Naming

  • feature/add-user-search — new functionality
  • fix/login-timeout-error — bug fixes
  • refactor/extract-auth-service — code restructuring
  • chore/update-dependencies — maintenance
  • hotfix/critical-payment-bug — production fixes

Commit Messages

# Conventional Commits format
type(scope): description

# Examples
feat(auth): add OAuth2 login with Google
fix(api): handle null response from payment gateway
refactor(orders): extract discount calculation into service
docs(readme): add deployment instructions
test(users): add integration tests for user search
chore(deps): upgrade FastAPI to 0.110.0
Good commit messages Focus on the why, not the what. The diff shows what changed. The commit message should explain why the change was necessary.

Pull Request Checklist

  • PR title clearly describes the change
  • Description includes context and motivation
  • All tests pass (unit + integration)
  • No unrelated changes (no "while I was here" cleanup)
  • Breaking changes are documented
  • Database migrations are reversible
  • Secrets and credentials are not committed

Code Review Guidelines

What to Look For

  • Correctness: Does the code do what it's supposed to? Edge cases handled?
  • Security: SQL injection? XSS? Unvalidated input? Exposed secrets?
  • Performance: N+1 queries? Unnecessary allocations? Missing indexes?
  • Readability: Can a new team member understand this in 5 minutes?
  • Simplicity: Is there a simpler way to achieve the same result?
  • Testing: Are the important paths tested? Are tests reliable?

Giving Feedback

  • Prefix with intent: nit: (minor), suggestion: (take it or leave it), blocking: (must fix)
  • Explain why: Don't just say "change this" — explain the reasoning
  • Offer alternatives: Show code, not just criticism
  • Praise good code: Highlight clever solutions and well-written tests

Time Complexity Cheat Sheet

OperationArrayLinked ListHash MapBST (balanced)
Access by indexO(1)O(n)N/AN/A
SearchO(n)O(n)O(1) avgO(log n)
Insert (end)O(1)*O(1)O(1) avgO(log n)
Insert (middle)O(n)O(1)N/AO(log n)
DeleteO(n)O(1)O(1) avgO(log n)

*Amortized. Occasional O(n) resize.

Common Algorithm Complexities

AlgorithmBestAverageWorstSpace
Binary SearchO(1)O(log n)O(log n)O(1)
Quick SortO(n log n)O(n log n)O(n²)O(log n)
Merge SortO(n log n)O(n log n)O(n log n)O(n)
Heap SortO(n log n)O(n log n)O(n log n)O(1)
BFS / DFSO(V+E)O(V+E)O(V+E)O(V)
DijkstraO(V+E log V)O(V+E log V)O(V+E log V)O(V)

HTTP Status Codes

CodeNameWhen to Use
200OKSuccessful GET, PUT, PATCH
201CreatedSuccessful POST that created a resource
204No ContentSuccessful DELETE (no body)
301Moved PermanentlyResource URL has permanently changed
304Not ModifiedClient cache is still valid (ETag match)
400Bad RequestMalformed request syntax, invalid body
401UnauthorizedMissing or invalid authentication
403ForbiddenAuthenticated but lacking permission
404Not FoundResource doesn't exist
409ConflictState conflict (duplicate, version mismatch)
422Unprocessable EntityValidation error (well-formed but invalid)
429Too Many RequestsRate limited
500Internal Server ErrorUnexpected server failure
502Bad GatewayUpstream service returned invalid response
503Service UnavailableServer is overloaded or in maintenance
504Gateway TimeoutUpstream service didn't respond in time

SQL Cheat Sheet

Essential Queries

-- Filtering and sorting
SELECT name, email, created_at
FROM users
WHERE active = true AND role IN ('admin', 'editor')
ORDER BY created_at DESC
LIMIT 20 OFFSET 40;

-- Aggregation
SELECT department, COUNT(*) AS total, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
HAVING COUNT(*) > 5
ORDER BY avg_salary DESC;

-- Joins
SELECT o.id, o.total, u.name, u.email
FROM orders o
INNER JOIN users u ON u.id = o.user_id
WHERE o.status = 'completed'
AND o.created_at >= NOW() - INTERVAL '30 days';

-- Subquery: users with above-average order count
SELECT name, order_count
FROM (
    SELECT u.name, COUNT(o.id) AS order_count
    FROM users u
    LEFT JOIN orders o ON o.user_id = u.id
    GROUP BY u.name
) sub
WHERE order_count > (SELECT AVG(c) FROM (SELECT COUNT(*) c FROM orders GROUP BY user_id) t);

-- Window function: rank employees by salary within department
SELECT name, department, salary,
       RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rank
FROM employees;

Index Guidelines

  • Index columns used in WHERE, JOIN, ORDER BY
  • Use composite indexes for multi-column filters (leftmost prefix rule)
  • Avoid indexing low-cardinality columns (e.g., boolean flags)
  • Use EXPLAIN ANALYZE to verify index usage
  • Partial indexes for filtered subsets: CREATE INDEX ... WHERE active = true

Best Practices Summary

AreaDoDon't
NamingDescriptive, intention-revealing namesSingle-letter variables, abbreviations
FunctionsSmall, single-purpose, few paramsGod functions, 10+ parameters
Error HandlingSpecific exceptions, log contextBare except, swallow errors silently
DependenciesInject via constructor, use interfacesHard-code concrete implementations
TestingTest behavior, use real dependenciesTest implementation, mock everything
API DesignConsistent naming, proper status codes200 for everything, verbs in URLs
SecurityValidate at boundaries, parameterize queriesTrust user input, string concatenation in SQL
GitSmall focused commits, descriptive messagesHuge commits, "fix stuff" messages
DocumentationExplain why, document decisionsRestate code, outdated comments
PerformanceMeasure first, optimize bottlenecksPremature optimization everywhere
© 2024 DevNextGen. All rights reserved.  |  Built with care for developer experience.