Backend Patterns

Implement and automate scalable backend architectural patterns for robust system development

Backend Patterns is an AI skill that provides established architectural patterns and implementation strategies for building server-side applications. It covers API design, data access layers, authentication flows, error handling, caching strategies, and service decomposition that enable developers to build maintainable and scalable backend systems.

What Is This?

Overview

Backend Patterns provides structured approaches to implementing common server-side architectures. It handles designing RESTful and GraphQL APIs with consistent endpoint conventions, implementing repository and service layers for data access separation, building authentication and authorization flows with middleware patterns, structuring error handling with typed responses and status codes, applying caching at multiple layers including HTTP, application, and database query levels, and decomposing monolithic applications into service boundaries.

Who Should Use This

This skill serves backend developers establishing application architecture, tech leads defining patterns for team adoption, full-stack developers building API services, and architects designing service decomposition strategies.

Why Use It?

Problems It Solves

Without established patterns, backend code mixes business logic with data access and HTTP handling. Inconsistent error responses across endpoints confuse API consumers. Ad-hoc caching creates cache invalidation bugs that produce stale data. Tightly coupled services make it difficult to modify or replace individual components.

Core Highlights

Layered architecture separates HTTP handling, business logic, and data access for testability. Consistent error handling returns typed responses with appropriate HTTP status codes. Middleware patterns apply cross-cutting concerns without duplicating code across routes. Repository abstraction decouples business logic from specific database implementations.

How to Use It?

Basic Usage

from dataclasses import dataclass
from abc import ABC, abstractmethod

@dataclass
class User:
    id: str
    email: str
    name: str

class UserRepository(ABC):
    @abstractmethod
    def find_by_id(self, user_id: str) -> User | None:
        pass

    @abstractmethod
    def find_by_email(self, email: str) -> User | None:
        pass

    @abstractmethod
    def save(self, user: User) -> User:
        pass

class UserService:
    def __init__(self, repo: UserRepository):
        self.repo = repo

    def get_user(self, user_id: str) -> User:
        user = self.repo.find_by_id(user_id)
        if not user:
            raise ValueError(f"User {user_id} not found")
        return user

    def register(self, email: str, name: str) -> User:
        existing = self.repo.find_by_email(email)
        if existing:
            raise ValueError("Email already registered")
        import uuid
        user = User(id=str(uuid.uuid4()), email=email, name=name)
        return self.repo.save(user)

Real-World Examples

from functools import wraps
import json
import time

class AppError(Exception):
    def __init__(self, message, status=400, code="BAD_REQUEST"):
        self.message = message
        self.status = status
        self.code = code

    def to_response(self):
        return {
            "error": {"code": self.code, "message": self.message}
        }, self.status

class CacheLayer:
    def __init__(self, default_ttl=300):
        self.store = {}
        self.ttl = default_ttl

    def get(self, key):
        entry = self.store.get(key)
        if entry and time.time() < entry["expires"]:
            return entry["value"]
        return None

    def set(self, key, value, ttl=None):
        self.store[key] = {
            "value": value,
            "expires": time.time() + (ttl or self.ttl)
        }

    def invalidate(self, key):
        self.store.pop(key, None)

def cached(cache, key_fn, ttl=None):
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            key = key_fn(*args, **kwargs)
            result = cache.get(key)
            if result is not None:
                return result
            result = fn(*args, **kwargs)
            cache.set(key, result, ttl)
            return result
        return wrapper
    return decorator

cache = CacheLayer(default_ttl=60)

@cached(cache, lambda user_id: f"user:{user_id}")
def get_user_profile(user_id):
    return {"id": user_id, "name": "Alice"}

profile = get_user_profile("123")
print(profile)

Advanced Tips

Use dependency injection to pass repository and service instances, enabling test doubles without monkey patching. Apply cache-aside pattern where the application checks cache before querying the database and populates cache on miss. Structure middleware in a pipeline so each concern like authentication, logging, and rate limiting can be composed independently.

When to Use It?

Use Cases

Use Backend Patterns when establishing architecture for new API services, when refactoring monolithic handlers into layered components, when adding caching to reduce database load on frequently accessed endpoints, or when standardizing error responses across an API surface.

Related Topics

Clean architecture principles, domain-driven design, middleware pipeline patterns, repository pattern implementation, and API versioning strategies complement backend pattern adoption.

Important Notes

Requirements

Understanding of the target framework request lifecycle. Database or ORM layer for repository implementations. Test infrastructure supporting dependency injection.

Usage Recommendations

Do: separate business logic from framework-specific code to enable testing without HTTP setup. Use typed error classes with status codes for consistent API error responses. Cache at the appropriate layer based on data volatility and access patterns.

Don't: add abstraction layers before understanding the actual access patterns and complexity. Cache mutable data without a clear invalidation strategy. Mix database queries directly into route handlers, which couples data access to the HTTP layer.

Limitations

Pattern overhead may be unnecessary for simple CRUD applications with minimal business logic. Repository abstraction adds indirection that can obscure query behavior for complex queries. Layered architecture requires discipline to maintain boundaries as applications grow.