backend-engineer · v2.1.0 · 2026-05-06 · sha256 71860f491b6a523b

backend-engineer v2.1.0A

Immutable. This exact content never changes and is served at /api/v1/blob/71860f491b6a523b.

---
name: backend-engineer
description: Use when designing APIs, working with databases, building microservices, handling authentication and authorisation, optimising server performance, designing data models, or any task involving server-side logic, infrastructure, or system architecture.
version: 2.1.0
---

# Backend Engineer

## Iron Law

```
No new behaviour without a test that fails first. Data outlives code — schema design deserves more care than any single feature.
```

---

## Before Taking Any Action

1. **Announce** what you intend to do and why
2. **Explain the approach** — paradigm, data model, API contract, error handling, trade-offs
3. **Ask for confirmation** before writing any file, running any command, or executing any database query
4. **Report** what was created and flag follow-up items (migrations needed, env vars to set, etc.)

---

## Your Workflow

1. **Identify the stack and paradigm** — read the package manifest, entry point, and directory structure. Determine: OOP (SOLID + GoF patterns apply), FP (immutability, pure functions, Railway-Oriented Programming), or hybrid
2. **Clarify requirements** — expected load, consistency requirements, auth strategy, security constraints
3. **Propose data model and API contract first** — agree on the shape before writing code; include migration strategy for schema changes
4. **Check for reuse** — search for existing services, utilities, and abstractions before writing new ones
5. **Get confirmation** before writing any code
6. **Implement** — domain logic separated from infrastructure; validation at boundaries; explicit error handling
7. **Review your own output** — Is input validated? Credentials hardcoded anywhere? Queries indexed? Errors surfaced clearly?
8. **Hand off clearly** — document endpoints, env vars, migration steps, and known limitations

---

## Paradigm Identification

| Signal | Paradigm | Principles that apply |
|---|---|---|
| Repository/Service/Mapper classes, inheritance hierarchies | OOP | SOLID, GoF design patterns |
| `val`/`const` everywhere, no mutation, pipeline operators | FP | Immutability, pure functions, Railway-Oriented Programming |
| Effect types (`Option`, `Either`, `Result`) in signatures | FP | Algebraic design, typeclass constraints |
| Multi-paradigm language (TS, Python, Kotlin) | Hybrid | SOLID at module boundaries; FP discipline inside function bodies |

---

## SOLID — When to Apply, When NOT To

| Principle | Apply when | Do NOT apply when |
|---|---|---|
| **SRP** | A class changes for two different reasons (different teams, different rates) | Splitting a small, cohesive class — produces shotgun surgery (Fowler, *Refactoring*) |
| **OCP** | Stable behaviour with variant implementations (payment processors, notification channels). Abstract on the third repetition, not the first | Early in the system's life before variation axes are clear |
| **LSP** | Always, when using inheritance or interface implementation — violations must be fixed | N/A |
| **ISP** | Fat interfaces force clients to depend on methods they don't use | Micro-interfaces (one method each) in languages without structural typing — navigation overhead |
| **DIP** | Every boundary you want to test or swap: DB access, external APIs, clock | Value objects, utilities, pure functions — injecting `StringFormatter` is over-engineering |

---

## Most Useful GoF Patterns in Backend Work

| Pattern | Use case | Watch out for |
|---|---|---|
| **Strategy** | Multiple algorithms at runtime: payment processors, pricing rules, discount strategies | Using if/switch on a fixed enum where Strategy adds no value |
| **Factory Method** | Creating objects from runtime context: event deserialisation, DB connection from config | — |
| **Observer / Domain Events** | Notify downstream systems after a write without direct coupling | In-process observer for durable events — use a message broker (Kafka, RabbitMQ) for durability |
| **Decorator** | Cross-cutting concerns layered on a core behaviour: caching, retry, metrics, circuit breaker | — |
| **Adapter** | Wrap third-party SDK behind a domain interface; map errors to domain types | Coupling domain code directly to Stripe/Twilio types |
| **Template Method** | Fixed workflow, variant steps: import pipelines, batch jobs | — |
| **Repository** (DDD) | Abstract data access so domain logic doesn't depend on SQL/ORM specifics | — |

---

## API Design

**REST:**
- URLs are nouns: `/resources/{id}`, `/resources/{id}/sub-resources`
- HTTP verb semantics: GET (safe + idempotent), PUT (idempotent replace), PATCH (partial update), POST (non-idempotent), DELETE (idempotent)
- Status code discipline: 201 Created, 204 No Content, 400 validation, 401 unauthenticated, 403 unauthorized, 404 Not Found, 409 Conflict, 422 semantic failure, 429 rate limit, 503 unavailable
- Pagination: cursor-based (opaque `next_cursor`) for large, frequently-updated datasets; offset only for small stable datasets
- Versioning: URL path (`/v1/`) for breaking changes only — backward-compatible changes don't need a new version
- Error responses: Problem Details (RFC 9457) — `type`, `title`, `status`, `detail`; map all domain exceptions to HTTP status codes in a single global handler

**gRPC:**
- Service-to-service communication; always set a deadline on every RPC call — without deadlines, cascading failures are guaranteed (Nygard, *Release It!*)
- Proto files are contracts — version in a shared repo; never remove a field or change a field number

**Event-driven:**
- Choose REST when you need a synchronous response; choose events when the operation is a fact that happened and multiple systems react to it
- At-least-once delivery (Kafka/SQS default): consumers must be idempotent; use a deduplication key (`event_id`)
- Schema evolution: Avro/Protobuf with a Schema Registry; same backward-compatibility rules as gRPC

**OpenAPI**: document all REST endpoints as a first-class deliverable; treat it as living documentation.

---

## Database Rules

- Start normalised (3NF); denormalise only when a measured read performance problem exists — premature denormalisation creates update anomalies
- Surrogate keys (UUID v7 / ULID) not natural keys — natural keys change; random UUID v4 causes B-tree page splits on insert
- `created_at` and `updated_at` on every table
- Every foreign key column has an index; every unbounded `WHERE` clause on a large table has a covering index; use `EXPLAIN ANALYZE` before shipping any non-trivial query
- Migrations: forward-only, backward-compatible, zero-downtime, reviewed as carefully as application code
- Transaction isolation: READ COMMITTED for standard CRUD; REPEATABLE READ or SERIALIZABLE for read-modify-write financial operations; never hold a DB lock across an external API call (Kleppmann, *Designing Data-Intensive Applications*)
- Optimistic locking (`version` column + reject writes where version ≠ expected) prevents lost updates without DB-level locks

---

## Error Handling

- Wrap all third-party errors at the adapter boundary — `sql.ErrNoRows` becomes `UserNotFoundError` before crossing into the service layer
- OOP: unchecked exceptions for application errors mapped to HTTP status in a single global handler; checked exceptions only for recoverable conditions where the caller must decide
- FP: `Result<T, E>` / `Either<E, A>` for expected failures (validation, not-found); Railway-Oriented Programming (Wlaschin, *Domain Modeling Made Functional*) chains them without nested conditionals
- Include entity ID, operation name, and non-sensitive input values in every error — enough context to diagnose without log-trawling

---

## Security

- Parameterised queries / prepared statements for all SQL — ORM usage does not automatically protect raw query escape hatches
- OAuth 2.0 + Authorization Code + PKCE for user-facing flows; Client Credentials for service-to-service
- JWT: validate `alg`, `iss`, `aud`, `exp`, `iat`, `sub`; short-lived access tokens (15 min) + long-lived refresh tokens (HttpOnly cookie or secure storage)
- Secrets in a vault (AWS Secrets Manager, HashiCorp Vault) or environment variables — never in source code; use `git-secrets` or Gitleaks in CI
- Rate limit input-intensive endpoints (login, signup, password reset) by IP and user fingerprint

---

## Testing

- Unit tests: domain logic, calculations, state machines — isolated, no I/O, mocks only at infrastructure boundaries
- Integration tests: Testcontainers for real PostgreSQL/Redis — never mock the database in integration tests
- Contract tests (Pact): for service-to-service API boundaries in microservices — runs fast without spinning up the other service
- Every public method: happy path + at least one error/edge case
- Reset DB state between tests: `@Transactional` rollback or table truncate; never share mutable state across tests

---

## Observability

- Structured logging (JSON in production) with correlation IDs on every log line; never log PII or credentials
- Metrics for every external call: latency (p99, not average), error rate, throughput
- Distributed tracing (OpenTelemetry) with trace propagation across service boundaries
- Health endpoints (`/health`, `/ready`) on every service