Developer Documentation
Comprehensive guides on software engineering principles, design patterns, architectural patterns, and best practices for building production-ready applications.
⚖
SOLID Principles
Five fundamental principles of object-oriented design for maintainable software.
Foundations
🔧
Design Patterns
Creational, structural, and behavioral patterns with real-world examples.
Patterns
🏗
Architecture Patterns
Clean Architecture, Microservices, Event-Driven, and CQRS patterns.
Architecture
🔗
API Design
RESTful conventions, versioning, pagination, and error response standards.
Best Practices
✅
Testing Strategies
Unit, integration, and E2E testing with the testing pyramid approach.
Quality
✨
Clean Code
Naming conventions, function design, and code readability principles.
Practices
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.
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): ...
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): ...
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: ...
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): ...
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
| Pattern | Category | Use When |
| Singleton | Creational | Exactly one instance needed (DB pool, config) |
| Factory | Creational | Object type determined at runtime |
| Builder | Creational | Complex object with many optional params |
| Adapter | Structural | Integrating incompatible interfaces |
| Decorator | Structural | Adding behavior without subclassing |
| Repository | Structural | Abstracting data access layer |
| Strategy | Behavioral | Swappable algorithms at runtime |
| Observer | Behavioral | Decoupled event notification |
| Iterator | Behavioral | Sequential access without exposing internals |
| Command | Behavioral | Encapsulate 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
| Method | Path | Action | Status |
GET | /users | List users | 200 |
POST | /users | Create user | 201 |
GET | /users/{id} | Get user | 200 |
PUT | /users/{id} | Replace user | 200 |
PATCH | /users/{id} | Update fields | 200 |
DELETE | /users/{id} | Delete user | 204 |
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
| Operation | Array | Linked List | Hash Map | BST (balanced) |
| Access by index | O(1) | O(n) | N/A | N/A |
| Search | O(n) | O(n) | O(1) avg | O(log n) |
| Insert (end) | O(1)* | O(1) | O(1) avg | O(log n) |
| Insert (middle) | O(n) | O(1) | N/A | O(log n) |
| Delete | O(n) | O(1) | O(1) avg | O(log n) |
*Amortized. Occasional O(n) resize.
Common Algorithm Complexities
| Algorithm | Best | Average | Worst | Space |
| Binary Search | O(1) | O(log n) | O(log n) | O(1) |
| Quick Sort | O(n log n) | O(n log n) | O(n²) | O(log n) |
| Merge Sort | O(n log n) | O(n log n) | O(n log n) | O(n) |
| Heap Sort | O(n log n) | O(n log n) | O(n log n) | O(1) |
| BFS / DFS | O(V+E) | O(V+E) | O(V+E) | O(V) |
| Dijkstra | O(V+E log V) | O(V+E log V) | O(V+E log V) | O(V) |
HTTP Status Codes
| Code | Name | When to Use |
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST that created a resource |
| 204 | No Content | Successful DELETE (no body) |
| 301 | Moved Permanently | Resource URL has permanently changed |
| 304 | Not Modified | Client cache is still valid (ETag match) |
| 400 | Bad Request | Malformed request syntax, invalid body |
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Authenticated but lacking permission |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | State conflict (duplicate, version mismatch) |
| 422 | Unprocessable Entity | Validation error (well-formed but invalid) |
| 429 | Too Many Requests | Rate limited |
| 500 | Internal Server Error | Unexpected server failure |
| 502 | Bad Gateway | Upstream service returned invalid response |
| 503 | Service Unavailable | Server is overloaded or in maintenance |
| 504 | Gateway Timeout | Upstream 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
| Area | Do | Don't |
| Naming | Descriptive, intention-revealing names | Single-letter variables, abbreviations |
| Functions | Small, single-purpose, few params | God functions, 10+ parameters |
| Error Handling | Specific exceptions, log context | Bare except, swallow errors silently |
| Dependencies | Inject via constructor, use interfaces | Hard-code concrete implementations |
| Testing | Test behavior, use real dependencies | Test implementation, mock everything |
| API Design | Consistent naming, proper status codes | 200 for everything, verbs in URLs |
| Security | Validate at boundaries, parameterize queries | Trust user input, string concatenation in SQL |
| Git | Small focused commits, descriptive messages | Huge commits, "fix stuff" messages |
| Documentation | Explain why, document decisions | Restate code, outdated comments |
| Performance | Measure first, optimize bottlenecks | Premature optimization everywhere |