# Backend Engineering

> Backend Engineering

- Skill: `handsomeboy990/backend-engineering` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add handsomeboy990/backend-engineering`
- Raw SKILL.md: https://api.skillmd.com/api/skills/handsomeboy990/backend-engineering/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Handsomeboy990 (https://skillmd.com/u/handsomeboy990)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/handsomeboy990/backend-engineering

---


# Backend Engineering

The server is the only place where a rule is actually enforced. Everything the
client does is a suggestion.

## 1. Layering

Three responsibilities, kept apart, whatever the framework calls them.

```
Handler   parse, validate, authenticate, authorize, call, map to a response
Service   business rules, invariants, orchestration, transaction boundary
Data      queries and persistence, no business rules
```

Rules for the handler: no business logic, no query construction, no more than
a screen of code. Rules for the service: no framework request or response
objects, no HTTP status codes. Rules for the data layer: no decisions.

When the project has fewer layers, follow the project. Do not introduce a
layer to satisfy a diagram; introduce it when a second caller appears.

## 2. Handler protocol

Every handler, in this order, without exception:

1. authenticate, reject before anything else;
2. validate the input against a schema, reject on failure;
3. authorize, including object level ownership;
4. call the service with typed, valid arguments;
5. map the result to a response shape the project already uses;
6. map failures to status codes deliberately;
7. log the failure with context, without secrets.

Skipping step 3 because step 1 passed is the most common serious defect in
backend code. Authentication says who; authorization says whether.

## 3. Never trust the client

Read from the server, never from the request:

prices and totals, currency, discounts, roles and permissions, ownership,
resource identifiers used for access decisions, workflow state transitions,
quotas and limits, timestamps that drive business rules, anything that grants
an advantage.

The request may carry an identifier and a quantity. Everything else is looked
up.

## 4. Database

**Queries.** Parameterised, always. Select the columns needed, not everything.
Every list query has a limit and a pagination contract. No query inside a
loop.

**Indexes.** Every column used for filtering, joining or ordering in a query
the feature adds. A foreign key without an index is a slow delete waiting to
happen. Composite index order matters: equality columns first, then range.

**Transactions.** Wrap exactly what must be atomic. No network call inside a
transaction. Keep them short; a transaction held across an external request is
a lock held across someone else's latency. Choose the isolation level
deliberately when the default does not prevent the anomaly at hand.

**Concurrency.** A check followed by a write is a race unless the database
enforces it. Prefer a unique constraint, a conditional update, or an atomic
increment over a read then write.

**Migrations.** Reversible, or explicitly marked irreversible with the reason.
Additive first: add the column, backfill, switch the reads, then drop the old
one, in separate deploys. Never rename and drop in the same migration that the
running code depends on. State the lock behaviour on large tables.

## 5. Errors

Two kinds, handled differently.

| Kind | Example | Response | Log level |
|---|---|---|---|
| expected | invalid input, not found, forbidden, conflict, quota | precise status, stable error shape | info or warn |
| unexpected | database down, unhandled defect, provider failure | generic message, no internals | error, with the full context |

Rules:

- one error shape across the API, matching what already exists;
- status codes chosen deliberately: 400 malformed, 401 unauthenticated, 403
  unauthorized, 404 absent or hidden, 409 conflict, 422 semantically invalid,
  429 limited, 500 unexpected;
- never leak a stack trace, a query, a file path or an internal identifier;
- never swallow an exception silently;
- an error that cannot be handled is propagated, not converted to a null that
  the caller will misread as absence.

## 6. External services

Decided before the first call, not after the first incident:

```
Timeout      a value, always, connect and total
Retry        bounded, exponential, jittered, idempotent operations only
Circuit      what happens when it is down repeatedly
Fallback     degrade, queue, or fail, chosen and stated
Idempotency  a key on any call that moves money or creates a resource
Verification signatures on inbound webhooks, before parsing
```

A call with no timeout is an outage waiting for the dependency to be slow
rather than down, which is the more common failure and the harder one.

## 7. Jobs, queues and scheduled work

- Every job is idempotent, because it will run twice.
- Every job is bounded in time and in the number of rows it touches.
- Failures are retried with a limit, then moved somewhere visible.
- A scheduled job that overlaps its previous run either locks or is designed
  to tolerate the overlap.
- Job arguments are identifiers, not serialised entities that go stale in the
  queue.
- Every job logs its start, its outcome and its counts.

## 8. Observability

Logs are for reconstruction, not decoration.

```
Structured   fields, not interpolated prose
Correlated   a request identifier that crosses the layers
Levels       error for unexpected, warn for expected failures worth seeing,
             info for state changes, debug off in production
Never        passwords, tokens, full bodies, personal data beyond what is
             needed to reconstruct
Metrics      counts and durations for the paths that matter
```

The test for a log line: during an incident, does it change what the operator
does next? If not, remove it.

## 9. API contracts

- Additive changes are safe: new optional fields, new endpoints.
- Breaking changes require a version, a deprecation period or a migration, and
  a documentation update in the same change.
- Response shapes are explicit field lists. Serialising an entity directly is
  how private fields reach clients.
- Pagination is a contract, not an implementation detail: state the shape,
  the limit and the maximum.
- Empty results are an empty collection with a 200, not a 404.

## 10. Protocol

1. Read the conventions and the layering the project actually uses.
2. Take the boundary decisions from `architecture-design` when the change
   creates one.
3. Write the schema, then the handler skeleton in the order of section 2.
4. Write the service with the business rules and the transaction boundary.
5. Write the data access, with the indexes the queries require.
6. Decide the error contract and the failure behaviour of every dependency.
7. Add logging that an operator can use.
8. Write the tests, including the negative and concurrent cases.
9. Run the suite and the repository checks.
10. Hand to `security-audit` and `code-review-protocol`.

## 11. Auto-critique

Score from 0 to 5: layering respected, handler order complete including
authorization, nothing trusted from the client, query and index quality,
transaction correctness, error contract consistency, dependency failure
handling, idempotency, log usefulness, test coverage of negative paths.

Threshold: no axis below 3, average at least 4. A missing authorization check
or an unparameterised query is an automatic failure.

## 12. Interfaces

- Upstream: `architecture-design`, `project-exploration`.
- Lateral: `input-validation`, `frontend-engineering` for the contract.
- Downstream: `security-audit`, `testing-quality`,
  `performance-engineering`, `code-review-protocol`,
  `technical-documentation`.

