A 200 OK is not the end of the story
Whether I am working in Go or Node, it is tempting to see a successful HTTP response as the end of a backend task. The route returns 200, the request passes a test, and the frontend can move on.
Over time, I have found that this is often only the beginning of the more important question: did the right thing happen, and would it still be right if the request arrived twice, failed halfway through, or was retried later?
A successful response can still hide a broken action
Consider an action that creates an order, sends a notification, or changes a user’s role. A user can double-click. A browser or network can retry a request. A worker can receive the same message more than once. If each attempt creates a new side effect, a 200 OK does not tell the full story.
For actions that should only happen once, I have found it useful to think about idempotency early. The exact approach depends on the action: an idempotency key, a unique business identifier, or a constraint that prevents a duplicate record. The important part is making the system safe when the same intent reaches it again.
This does not mean every endpoint needs the same machinery. A read request and a payment-like action carry very different risks. Naming that difference helps the team spend effort where a duplicate would actually matter.
Let Postgres protect important rules
Application code can validate input, but the database is often the last place that can protect an important rule when two requests arrive at nearly the same time.
Unique constraints, foreign keys, check constraints, and transactions have been some of the most useful tools for this. A unique constraint can stop duplicate data even if two application instances make the same decision at once. A transaction keeps related changes together, so the database does not end up halfway through an action.
-- The same intent can arrive twice; the database decides once
CREATE TABLE orders (
id uuid PRIMARY KEY,
user_id uuid NOT NULL REFERENCES users (id),
idempotency_key text NOT NULL,
total_cents integer NOT NULL CHECK (total_cents >= 0),
UNIQUE (user_id, idempotency_key)
);This has also changed how I think about schema design. A table is not only where data lives. In some cases, it is where the product’s non-negotiable rules should live too.
Treat failure as part of the normal path
Networks time out. Dependencies slow down. Deployments overlap with in-flight work. These are not unusual events, even if they do not happen every day.
For me, handling failure starts with a few practical questions:
- Can the caller retry safely?
- What should happen if the database change succeeds but an external service does not?
- What context would help someone understand the problem from the logs later?
Clear timeouts, structured logs, and error responses do not remove failures. They make a failure easier to diagnose and less likely to turn into a confusing experience for the user, or for the next engineer who investigates it.
Make the infrastructure part of the review
The same thinking carries into AWS and Terraform. An application change can depend on permissions, network rules, environment configuration, a queue, or a database setting. Keeping those changes in Terraform has helped me see infrastructure as part of the codebase, rather than something that only exists in the console.
A Terraform plan is not a guarantee that a change is safe, but it gives the team a concrete change to review: which resource will change, which access is being granted, and whether environments are still aligned. That is especially useful when a backend change crosses the boundary between application code and AWS.
A reliable backend is not built by adding every possible safeguard. It grows from understanding which actions carry risk, putting the right guardrails close to those actions, and making the failure paths visible enough to improve over time.
Working on something similar? Let's talk