Backend Service Capstone: Build, Ship, and Operate One API
LESSON
Backend Service Capstone: Build, Ship, and Operate One API
By the end of this lesson, you will be able to...
Review one backend request from API contract to durable state and production evidence.
Identify gaps that make a service look complete but unsafe to operate.
Use a readiness rubric to decide whether a small API is ready to ship.
Idea in one sentence: A backend is ready to ship when one important request can be explained from client promise to database state, tests, release path, and operational signals.
Core Insight
Imagine you are building the first orders API for a small shop.
The feature list looks ordinary:
create an order
read an order
list my orders
let admins manage products
store data in a relational database
run tests
build a container
deploy the service
emit logs and metrics
A beginner can turn that list into a set of disconnected tasks. One route returns 201. One table stores orders. One test passes. One image builds. One dashboard exists.
That can still produce a backend nobody trusts.
The capstone question is not:
Did each checklist item appear somewhere?
The better question is:
Can one important request be explained end to end?
Use POST /orders.
A client sends JSON. The backend authenticates the caller, checks authorization, validates input, reads products, calculates the total, writes durable rows, prevents duplicate retries, returns a clear response, proves the behavior with tests, ships a repeatable artifact, and leaves evidence that can explain success or failure in production.
The mental model is:
one request -> many boundaries -> one chain of evidence
Each boundary asks a different question:
contract: what did we promise the client?
identity: who is making the request?
validation: is the input acceptable?
database: what became durable?
tests: what evidence do we have before release?
deployment: what artifact is running?
observability: what happened after users touched it?
If those answers agree, the service is small but coherent. If they disagree, adding more endpoints usually makes the service harder to trust.
The Scenario
Build a small API with five routes:
POST /orders
GET /orders/:id
GET /orders
POST /admin/products
PATCH /admin/products/:id
The service owns:
orders
order items
product catalog
It does not own:
payment processing
email delivery
warehouse fulfillment
Those can be mocked, queued, or integrated later. Ownership matters because a backend that owns everything soon has no clear boundary.
A minimal relational model might be:
users(id, email, role)
products(id, sku, name, price_cents, active)
orders(id, user_id, status, total_cents, idempotency_key, created_at)
order_items(id, order_id, product_id, quantity, unit_price_cents)
The capstone focuses on one request:
POST /orders
Request:
Authorization: Bearer <token>
Idempotency-Key: <client-generated-key>
{
"items": [
{ "product_id": "p1", "quantity": 2 }
]
}
Success:
201 Created
{
"order_id": "ord_123",
"status": "created",
"total_cents": 5000
}
Expected client errors:
400 malformed JSON or invalid quantity
401 missing or invalid authentication
403 authenticated user is not allowed
404 product does not exist or is not visible
409 same idempotency key with different input
This contract is not documentation decoration. It is the shared promise that shapes handler code, database constraints, tests, release checks, and observability.
Plain meaning:
A service boundary says what this backend owns, promises, rejects, and records.
In this scenario:
The orders API owns order creation and product catalog state, but not payment settlement or email delivery.
Technical name:
This is the backend service boundary.
The Naive Completion
The tempting version is:
write a handler
insert an order row
return 201
manually click the route once
ship it
That feels like progress because the happy path works.
It breaks when ordinary backend pressure arrives:
the client retries after a timeout
two requests arrive close together
the product price changes
the user tries to read someone else's order
the migration runs before every instance is updated
support asks what happened to request req_982
The happy-path handler cannot answer those questions by itself.
The missing piece is not a framework feature. The missing piece is an end-to-end chain where each layer reinforces the same promise.
Check: Why is "the handler returned 201 once" weak evidence for this capstone?
Think first, then reveal.
Answer: It proves only one local happy path. It does not prove authorization, validation, durable constraints, retry behavior, migration compatibility, release readiness, or production evidence.
Walkthrough: One Request End to End
Trace POST /orders as a review, not as code trivia.
Input:
user u17
role customer
idempotency key key_abc
body asks for 2 units of product p1
Step 1: route and contract
route matches POST /orders
service expects JSON body and Idempotency-Key header
Intermediate state:
the backend knows which operation the client requested
Step 2: authentication and authorization
token maps to user u17
policy allows customers to create their own orders
Intermediate state:
the request has an actor and an allowed action
Step 3: validation
product_id is present
quantity is a positive integer
client did not send trusted price_cents
Intermediate state:
input shape is acceptable, but no durable state has changed
Step 4: transaction
find active product p1
read price from database
compute total from database price
insert order for u17
insert order item
enforce unique (user_id, idempotency_key)
commit
Intermediate state:
order and items are durable together
Step 5: response
return 201 with order id, status, and total
Step 6: evidence
integration test proves retry behavior
release artifact has version abc123
log, metric, trace, and event explain the request
Output:
one order exists
duplicate retry does not create another order
support can investigate the request later
Now compare the broken version:
Input:
same request arrives twice after a client timeout
Naive transition:
handler inserts order without idempotency key
handler inserts another order on retry
Output:
customer may be charged or fulfilled twice
support sees two orders but cannot tell which one is the retry
The capstone standard is not "add idempotency as a word." It is:
contract requires key
handler uses key
database enforces uniqueness
integration test retries the route
observability emits idempotency_replayed or conflict evidence
So far, the service is becoming coherent. One behavior is visible across API, code, database, tests, and operations.
Evidence Before Release
A small backend needs evidence before production traffic reaches it.
Use the right evidence for the right boundary.
unit tests:
total calculation
validation helper
role decision helper
domain error to HTTP status mapping
integration tests:
real route
auth middleware
database transaction
foreign keys and uniqueness constraints
migration-applied schema
idempotent retry through HTTP
contract checks:
request schema
response body
expected status codes
stable error shape
release checks:
migration runs from previous schema
container starts with required configuration
artifact is tagged with commit or digest
rollout watches route success and error rate
A handler-only unit test is still useful. It is just not enough.
For POST /orders, the strongest beginner-level test is an integration test:
start test server
apply migrations
create user and product fixtures
send POST /orders with idempotency key
assert response is 201
assert one order row and one item row exist
send same request again with same key
assert existing order is returned or replayed safely
assert no duplicate order exists
send same key with different body
assert 409 Conflict
That test proves the route, middleware, transaction, database constraint, and response mapping together.
Check: Which layer should make duplicate order creation impossible: client, handler, database, or test?
Think first, then reveal.
Answer: The design should use all of them together. The client sends an idempotency key, the handler uses it, the database enforces uniqueness, and tests prove the behavior through the real route.
Failure Review
A capstone should review failure before release, not after surprise.
Here are useful failure questions for POST /orders:
Bad JSON:
Do we return 400 with a stable error body?
Missing token:
Do we return 401 instead of creating anonymous state?
Wrong user:
Can one user read or modify another user's order?
Inactive product:
Do we reject the order before writing partial state?
Duplicate retry:
Do we replay safely or return a clear conflict?
Database unavailable:
Do we fail clearly and emit dependency evidence?
Migration missing:
Does deployment fail fast instead of serving broken traffic?
Payment provider slow:
Does the API behavior match the contract, and can operators see the dependency delay?
The important pattern is:
failure -> expected response -> durable state -> evidence
For example:
failure:
same idempotency key, different body
expected response:
409 Conflict
durable state:
no new order is created
evidence:
log event=idempotency_conflict request_id=req_912 route=POST /orders
metric idempotency_conflicts_total{route=POST /orders} += 1
trace shows transaction rolled back
That is stronger than "an error happened." It says what the service promised, what state changed, and what evidence remains.
Trade-offs
The central trade-off is shipping speed versus confidence under real conditions.
You can ship faster by skipping API contracts, database constraints, integration tests, migration checks, release metadata, and observability. The service may look done sooner. The cost is that uncertainty moves into production.
This capstone discipline improves:
- client clarity
- durable state integrity
- retry safety
- release confidence
- incident investigation
It costs:
- more design before coding
- slower tests for important boundaries
- migration planning
- operational naming discipline
- more review of privacy and secret handling
It can still fail when:
- the external payment provider behaves differently from the mock
- a migration plan ignores an old worker
- observability misses a rare path
- tests use fixtures that hide real data problems
- alerts exist but nobody owns them
You can see the boundary when the team can answer local code questions but cannot answer production questions:
Which version handled the failed request?
Did the transaction commit?
Was this a retry?
Which invariant protected the data?
Which test would fail if this behavior broke?
The goal is not perfect certainty. It is enough evidence to make risk visible before users do the discovery work.
Evidence and Readiness
Use this readiness table before calling the API shippable.
| Boundary | Ready Signal | Not Ready Signal |
|---|---|---|
| Contract | Important routes have request, response, and error shapes | Clients must read handler code to guess behavior |
| Identity | Backend authorizes from authenticated user and action | Frontend hides buttons and backend trusts it |
| Validation | Invalid input returns stable 400-style errors | Bad input reaches database or becomes 500 |
| Durable state | Constraints protect facts that must stay true | Correctness depends only on application hope |
| Retry | Idempotency behavior is designed and tested | Duplicate requests create duplicate orders |
| Tests | Integration test crosses route and database boundary | Only handler helpers are tested |
| Configuration | Secrets and environment-specific values are outside code | Local config is baked into the artifact |
| Release | Artifact, migration, and rollout checks are repeatable | Deployment depends on manual local steps |
| Observability | Logs, metrics, traces, and events explain one request | Support cannot connect user report to backend path |
This table is deliberately practical. It does not ask whether the service uses a fashionable architecture. It asks whether the important behavior can survive contact with users.
Use the table from left to right.
Start with the contract because every later layer depends on the promise. If the contract is vague, the database cannot know which facts matter, tests cannot know what to assert, and observability cannot know which signals describe success.
Then check identity and validation. These are the first trust boundaries. A request from the internet is not yet a command the service should obey. The backend must decide who is acting, whether the action is allowed, and whether the input shape is acceptable.
Only then review durable state. This is where beginner services often look fine until concurrency or retry pressure appears. The database should protect facts that must remain true even when two requests arrive at the same time. Application code is still important, but durable constraints are the final guardrail for durable facts.
After that, review tests and release. A service that works on one laptop is not the same as a service that can be rebuilt, migrated, deployed, and rolled out with evidence. The artifact should be repeatable. The migration should be compatible. The release should have a stop signal.
Finally, review observability. This is where the service proves it can be operated after the happy path. If a user says "my order failed," the team should not need to guess. The request ID, route, release, database outcome, dependency outcome, and domain event should give a first investigation path.
That order matters. It keeps the capstone from becoming a vague checklist. Each layer asks a more specific version of the same question:
What did we promise?
How do we enforce it?
How do we prove it?
How do we explain it later?
A Review Walkthrough
Imagine a teammate says:
I finished POST /orders.
Can you review whether it is ready?
A weak review looks only at the handler:
Does the code look clean?
Does it return 201?
A stronger review follows the chain.
First, ask for the contract. You want to see the request body, required headers, success response, and error responses. If the idempotency key is optional, ask what happens when the client retries. If error bodies are inconsistent, ask how clients will recover.
Second, ask about identity. Which user is authenticated? Which role may create an order? Is authorization enforced on the backend, or only implied by the frontend? The backend should not trust a hidden button. A user can call an API without using your UI.
Third, ask about validation. The client can send malformed JSON, negative quantities, unknown product IDs, or fields the service should ignore. The handler should reject bad input before writing durable state. It should not let accidental input become a partial order.
Fourth, ask about the transaction. Where does the service read the product price? When does it compute the total? What happens if one item is invalid? What happens if the second insert fails? The answer should include a transaction boundary and database constraints, not only hopeful application code.
Fifth, ask about retry. A timeout does not mean the server did nothing. The client may retry after the order already committed. The service needs a stable idempotency rule so the same request does not create a second order.
Sixth, ask about evidence before release. Which test crosses the real route and database boundary? Which test proves duplicate retry behavior? Which check proves the migration path works from the previous schema?
Seventh, ask about evidence after release. Which log fields would support need? Which metric would show user-visible harm? Which trace span would show whether the database or payment provider was slow?
This review style is slower than glancing at the handler. It is also the difference between "the code looks plausible" and "the service behavior is explainable."
Check: During review, you find that POST /orders has a contract and unit tests, but no integration test through the route and database. What risk remains?
Think first, then reveal.
Answer: The handler logic may be tested, but the real boundary may still fail. Middleware, auth, validation wiring, migrations, database constraints, transactions, and HTTP response mapping may not work together.
Final Challenge
Review this proposed implementation:
POST /orders:
accepts JSON body with product_id and quantity
trusts price_cents from the client
creates an order row before inserting items
has a unit test for total calculation
has no idempotency key
logs "order failed" on exceptions
deploys from a developer laptop
Use the capstone rubric. Name at least six problems and propose a safer replacement for each.
A strong answer should mention:
- The client should not provide trusted prices; the service should read product prices from the database.
- The request needs a written contract with status codes and error bodies.
- Order and item writes should share a transaction.
- Durable constraints should protect foreign keys, positive quantities, and idempotency uniqueness.
- Duplicate retries need an idempotency key across contract, handler, database, and tests.
- Unit tests are useful but not enough; add integration tests through route and database.
- Logs need structured fields such as request ID, route, status, release, and error code.
- Deployment should use a repeatable artifact from CI, not a developer laptop.
- Secrets and environment-specific configuration should come from the environment or platform.
- Release checks should include migration compatibility and route-level health signals.
Now do the same review for PATCH /admin/products/:id.
Ask:
Who is allowed?
What fields may change?
Which database facts must remain true?
What response does the client expect?
Which tests prove denied and allowed cases?
What signal explains a failed admin update?
If you can answer those questions for a second route, you are not just memorizing this capstone. You are transferring the backend review method.
That transfer is the point of the track. The exact tables, routes, framework, and database will change from project to project. The review shape should remain familiar: name the promise, protect the state, prove the behavior, ship the artifact, and leave evidence. When a new backend feels messy, return to one request and make the chain visible again.
Resources
- [DOC] [OpenAPI Specification]
- Link: https://spec.openapis.org/oas/latest.html
- Focus: Use this to describe request and response contracts that clients and tests can share.
- [DOC] [PostgreSQL Tutorial: Transactions]
- Link: https://www.postgresql.org/docs/current/tutorial-transactions.html
- Focus: Review how grouped database changes commit or roll back together.
- [ARTICLE] [The Twelve-Factor App: Config]
- Link: https://12factor.net/config
- Focus: Connect deployable services to environment-based configuration and secret hygiene.
- [DOC] [Docker: What Is an Image?]
- Link: https://docs.docker.com/get-started/docker-concepts/the-basics/what-is-an-image/
- Focus: Treat the container image as the repeatable artifact that moves from CI to deployment.
- [DOC] [OpenTelemetry Observability Primer]
- Link: https://opentelemetry.io/docs/concepts/observability-primer/
- Focus: Connect logs, metrics, and traces to production questions about service behavior.
Key Takeaways
- A backend capstone should trace one important request through contract, identity, validation, durable state, response, tests, release, and observability.
- A service boundary is useful only when the API contract, database constraints, tests, and operational signals agree about the same behavior.
- Idempotency, authorization, migrations, configuration, and traces are not advanced extras; they are how a small service becomes safe enough to ship.
- The readiness question is not "does the happy path work once?" but "can we prove what should happen and explain what did happen?"
← Back to Backend Development Foundations