ArchitectMind
  1. Home
  2. Tutorials
  3. Book 6 · Databases, APIs & Integration
  4. Volume V
  5. Status Codes
Book 6 · Volume V — API Design, HTTP & REST · Topic 1.3

Status Codes

2xx, 3xx, 4xx and 5xx

8 min readStage 3 · Systems8 interview questions
Chapter
Chapter 1 — HTTP Fundamentals
Prerequisites
Topics 1.1, 1.2
You will be able to
Use each status class correctly, pick specific codes for common API outcomes, distinguish client errors from server errors for retries and monitoring, handle redirects and conditional responses, and implement consistent status handling in Spring Boot
Covers
2xx; 3xx; 4xx; 5xx
On this page
  1. 1. Concept / Definition
  2. 2. Why It Exists
  3. 3. Core Concepts
  4. 4. How It Works Internally — Status Handling in Spring
  5. 4A. Deep Dive — A Decision Procedure for Errors
  6. 5. APIs / Features / Syntax
  7. 6. Practical Examples
  8. 7. Common Problems & Mistakes
  9. 8. Performance & Production Considerations
  10. 9. Architecture & Design Considerations
  11. 10. Interview Questions & Answers

1Concept / Definition

A status code is a three-digit number in every HTTP response that tells the client (and intermediaries) the outcome of the request. The first digit defines the class.

Status code classes
Figure 1 — Status code classes
TOC classMeaningClient behaviour
2xxSuccessUse the response
3xxRedirection / cached copyFollow Location or use cache
4xxClient errorFix the request; don't retry unchanged (except 408/429 after waiting)
5xxServer errorRetry idempotent requests with backoff; alert operators
Table 1 — Status classes

2Why It Exists

Generic clients and infrastructure need a machine-readable outcome independent of the body format. Status codes let a retry library, a CDN, a load balancer and a monitoring system make correct decisions without parsing application payloads.

ConsumerUses status to…
Client SDKThrow typed exceptions, decide retries
CDN / cacheCache 200/301/404 as configured; never cache 5xx long
Load balancerMark instances unhealthy on 5xx rates
Monitoring/SLOsCount 5xx as errors, 4xx as client issues
BrowserFollow redirects, show auth prompts
Table 2 — Who reads status codes

3Core Concepts

3.1 2xx

2xx success codes
Figure 2 — 2xx success codes
SituationCode
GET found200 + body
POST created201 + Location (+ body)
Async processing started202 + status resource Location
DELETE done / PUT without echo204
Range download206
Table 3 — Choosing 2xx

3.2 3xx

3xx redirection codes
Figure 3 — 3xx redirection codes
SituationCode
Resource moved permanently (API path change)308 (preserves method)
After POST, see result elsewhere303
Conditional GET, unchanged304 (no body)
Temporary routing307
Table 4 — Choosing 3xx

API clients often don't follow redirects for unsafe methods — use 308/307 and document them; prefer stable URIs.

3.3 4xx

4xx client error codes
Figure 4 — 4xx client error codes
Common confusionGuidance
401 vs 403401 = not authenticated (send WWW-Authenticate); 403 = authenticated but not allowed
400 vs 422400 = malformed (bad JSON, wrong types); 422 = well-formed but violates business rules
404 vs 403 for others' resources404 avoids revealing existence (multi-tenant)
409 vs 412409 = conflicts with current state; 412 = client precondition (ETag) failed
429Rate limit; include Retry-After
Table 5 — 4xx decisions

3.4 5xx

5xx server error codes
Figure 5 — 5xx server error codes
RuleReason
Never expose stack tracesSecurity; use correlation ids
503 + Retry-After for overload/maintenanceClients back off correctly
502/504 from gatewaysUpstream problems — outcome may be unknown
Don't map business errors to 500Pollutes error budgets and alerts
Table 6 — 5xx practice

4How It Works Internally — Status Handling in Spring

MechanismResult
Return object from controller200 by default
ResponseEntity.status(…) / .created() / .noContent()Explicit status + headers
@ResponseStatus on handler or exceptionFixed status
ResponseStatusExceptionStatus from code
@RestControllerAdvice + @ExceptionHandlerMap exceptions → status + ProblemDetail (Chapter 4)
Built-in: validation → 400, missing handler → 404, unsupported media → 415Framework defaults (ResponseEntityExceptionHandler)
Spring Security401/403 via AuthenticationEntryPoint/AccessDeniedHandler
Table 7 — Where status codes come from
JAVA
@RestControllerAdvice
class ApiErrors {
    @ExceptionHandler(OrderNotFound.class)
    ProblemDetail notFound(OrderNotFound e) {
        return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND,
                                                "Order " + e.id() + " not found");
    }
    @ExceptionHandler(InvalidStateTransition.class)
    ProblemDetail conflict(InvalidStateTransition e) {
        return ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, e.getMessage());
    }
    @ExceptionHandler(BusinessRuleViolation.class)
    ProblemDetail unprocessable(BusinessRuleViolation e) {
        return ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY,
                                                e.getMessage());
    }
}

4ADeep Dive — A Decision Procedure for Errors

Choosing an error status
Figure 6 — Choosing an error status
Retry classification (client side)Codes
Never retry unchanged400, 401 (until re-auth), 403, 404, 405, 409, 410, 412, 415, 422
Retry after waiting408, 429 (Retry-After), 503 (Retry-After)
Retry idempotent with backoff500, 502, 504, connection errors
Unknown outcome504/timeouts on non-idempotent calls → use idempotency keys
Table 8 — Status codes drive retry policies

5APIs / Features / Syntax

Spring APIExample
ResponseEntity.created(uri).body(v)201
ResponseEntity.accepted().location(uri).build()202
ResponseEntity.noContent().build()204
ResponseEntity.status(HttpStatus.CONFLICT).body(problem)409
throw new ResponseStatusException(HttpStatus.NOT_FOUND)404
ResponseEntity.status(412).build() / checkNotModified(etag)412 / 304
Table 9 — Status APIs

6Practical Examples

6.1 Always-200 API

A legacy API returned 200 with {"success": false, "errorCode": "E102"} for all errors. The gateway cached error responses, retry libraries never retried outages, and dashboards showed 100% success during incidents. Introducing proper status codes (with a transition period and client SDK update) restored caching, retries and alerting.

6.2 Validation errors as 500

Unhandled ConstraintViolationExceptions produced 500s. The error budget burned on client typos and the on-call engineer was paged nightly. A @RestControllerAdvice mapping validation to 400/422 fixed alerts overnight.

6.3 Worked example — status map for an order API

SituationStatus
Order created201 + Location
Order fetched200
Unknown order404
Not your order (tenant)404
Invalid JSON400
Quantity > stock422
Cancel after shipping409
Stale ETag on update412
Too many requests429 + Retry-After
Payment provider timeout504 (or 202 + async status)
Table 10 — Consistent mapping per situation

6.4 Worked example — 202 Accepted for long work

JAVA
@PostMapping("/api/reports")
ResponseEntity<JobView> start(@Valid @RequestBody ReportRequest r) {
    JobView job = reports.start(r);                          // queued
    return ResponseEntity.accepted()
        .location(URI.create("/api/report-jobs/" + job.id()))
        .header(HttpHeaders.RETRY_AFTER, "5")
        .body(job);
}
Follow-upResponse
GET /api/report-jobs/77 while running200 {status: RUNNING, progress: 40}
When done200 {status: DONE, result: "/api/reports/77"} (or 303 to result)
Table 11 — Asynchronous processing with status codes

6.5 Worked example — 304 with Spring's ETag support

JAVA
@GetMapping("/api/products/{sku}")
ResponseEntity<ProductView> get(@PathVariable String sku, WebRequest request) {
    ProductView p = products.get(sku);
    String etag = "\"" + p.version() + "\"";
    if (request.checkNotModified(etag)) return null;          // Spring sends 304
    return ResponseEntity.ok().eTag(etag).body(p);
}
RequestResponse
First GET200 + ETag "12" + body
If-None-Match: "12"304, no body
Table 12 — Cheap revalidation

6.6 Worked example — 401 vs 403 in Spring Security

RequestResult
No/expired token401 + WWW-Authenticate: Bearer error="invalid_token"
Valid token, missing scope orders:write403
Valid token, other tenant's order404 (application decision)
Table 13 — Authentication vs authorisation vs visibility

6.7 Worked example — 429 with Retry-After

HTTP
HTTP/1.1 429 Too Many Requests
Retry-After: 30
RateLimit-Policy: 100;w=60
RateLimit: limit=100, remaining=0, reset=30
Content-Type: application/problem+json
 
{"type":"https://api.example.com/problems/rate-limit",
 "title":"Too many requests","status":429}

Clients (and SDKs) back off precisely instead of hammering.

6.8 Worked example — gateway-generated codes

CodeGenerated byMeaning for API teams
502Gateway/LBUpstream returned invalid response or closed connection
503GatewayNo healthy upstream / circuit open
504GatewayUpstream too slow (gateway timeout < service timeout?)
413GatewayPayload too large
Table 14 — Not every status comes from your code

Align timeouts: gateway timeout > service timeout > downstream timeouts, to avoid 504s for work that still completes.

6.9 Worked example — monitoring by status class

MetricAlert
5xx rate > 1% for 5 minPage on-call
4xx rate spike on one clientNotify client team (likely client bug)
429 rateCapacity/quotas review
401 spikeToken issuer/clock issues
Table 15 — Status classes power observability

6.10 Worked example — 410 Gone for retired resources

SituationResponse
Endpoint version retired410 + link to migration guide
Temporary unavailability503 + Retry-After
Resource deleted permanently and known410 (optional; 404 acceptable)
Table 16 — Communicate permanence

6.11 Worked example — client-side handling with RestClient

JAVA
OrderView o = client.get().uri("/api/orders/{id}", id)
    .retrieve()
    .onStatus(s -> s.value() == 404, (req, res) -> { throw new OrderNotFound(id); })
    .onStatus(HttpStatusCode::is5xxServerError, (req, res) -> {
        throw new UpstreamUnavailable(res.getStatusCode().value());
    })
    .body(OrderView.class);

Typed exceptions per status class let callers decide retries and fallbacks.

6.12 Worked example — status code review checklist

CheckPass
No 200 responses carrying errors✔
Creation returns 201 + Location✔
Validation → 400/422, never 500✔
Auth → 401 with WWW-Authenticate; permissions → 403/404✔
Concurrency → 409/412 documented✔
Rate limits → 429 + Retry-After✔
5xx bodies contain correlation id, no internals✔
Table 17 — Consistency across all endpoints

7Common Problems & Mistakes

  • 200 for errors (error flags in bodies).
  • 500 for client mistakes.
  • 401/403 confusion.
  • Missing Location on 201/202.
  • Redirects on APIs that clients don't follow for POST.
  • Leaking stack traces in 5xx responses.

8Performance & Production Considerations

AspectGuideline
Caching304 via ETag; don't cache 5xx
RetriesStatus-based classification in SDKs/gateways
SLOs5xx as errors; 4xx tracked separately
SecurityNo internal details; consistent 404 for hidden resources
ConsistencyCentral exception mapping
Table 18 — Operational guidelines

9Architecture & Design Considerations

  • Define an organisation-wide status code catalogue in API guidelines.
  • Implement mapping centrally (@RestControllerAdvice, shared library).
  • Align gateway, service and client behaviour on retries and timeouts.
  • Use status codes as the basis of error budgets and SLOs.

10Interview Questions & Answers

Q1BasicConcept

What does each status class mean?

The first digit sets the class, so even a client that does not know a specific code can react correctly. 1xx is informational, an interim response such as 100 Continue or 103 Early Hints before the final one. 2xx means success: use the response. 3xx means redirection or use a cached copy: follow Location or reuse what you have, as with 304. 4xx means the client made a mistake, so fix the request instead of retrying it unchanged, apart from 408 and 429 after waiting. 5xx means the server failed, so idempotent requests may be retried with backoff and operators should be alerted.

Q2BasicConcept

Which status should a successful POST that creates a resource return?

Return 201 Created with a Location header holding the URI of the new resource, for example /api/orders/5001. Most APIs also return a representation of the created resource in the body, so the client gets generated fields such as the id, timestamps and status without a second GET; adding an ETag lets the client start conditional updates straight away. In Spring, ResponseEntity.created(uri).body(view) does all of this, and ServletUriComponentsBuilder can build the URI from the current request. Use 200 only when the POST did not create a resource, and 202 when creation will happen asynchronously. Combine creation with an idempotency key so retries do not create duplicates.

Q3IntermediateTricky

What is the difference between 401 and 403?

401 Unauthorized actually means unauthenticated: the request has no valid credentials, for example a missing, expired or badly signed token, and the response must include a WWW-Authenticate header such as Bearer error="invalid_token" telling the client how to authenticate. Retrying after logging in or refreshing the token can succeed. 403 Forbidden means the server knows who the caller is but that identity is not allowed to do this, for example a token without the orders:write scope; re-authenticating will not help. For resources belonging to another tenant, many APIs return 404 instead of 403 so they do not reveal that the resource exists. Spring Security produces 401 and 403 through separate handlers.

Q4IntermediateScenario

When would you return 202?

Return 202 Accepted when the server has validated and queued the request but the actual work will finish later, such as report generation, bulk imports, video processing or a payment awaiting a slow provider. The response should not pretend the work is done. Include a Location header pointing to a status resource like /api/report-jobs/77, optionally a Retry-After hint, and a body with the job id and state. The client polls that resource, which returns RUNNING with progress and then DONE with a link to the result, or redirects with 303. This keeps HTTP connections short, avoids gateway 504 timeouts and fits naturally with queue-based processing behind the API.

Q5Senior EngineerTrade-off

When should you use 409 vs 412?

409 Conflict means the request conflicts with the current state of the resource according to business rules, regardless of what the client sent as preconditions: cancelling an order that has already shipped, or creating a username that is taken. 412 Precondition Failed is narrower: the client sent a conditional header such as If-Match with an ETag, and the current version no longer matches, which is optimistic concurrency at the HTTP level. A 412 tells the client to reload the resource, merge and retry; a 409 tells it the operation is not allowed in this state. If you require preconditions on updates, return 428 when the client omits them.

Q6Senior EngineerTricky

Why not return 200 with an error body?

Because everything outside your application code reads the status line, not your JSON. With 200 for errors, CDNs and gateways cache failures as if they were valid content, retry libraries never retry real outages, circuit breakers never open, and SLO dashboards report 100 percent success during an incident. Client SDKs cannot throw typed exceptions, so every caller must parse a custom error flag and some will forget, silently treating failures as success. Browser and HTTP tooling behaviour breaks too. Use proper 4xx and 5xx codes with an RFC 9457 Problem Details body for the detail. Migrating a legacy always-200 API needs a transition period and updated client SDKs.

Q7Technical ArchitectDesign

How do status codes relate to retry policies?

Status codes are the input to retry classification. Most 4xx responses, such as 400, 403, 404, 409 and 422, mean the request itself is wrong and will fail again, so never retry them unchanged; 401 can succeed only after re-authenticating. 408, 429 and 503 mean try later, so wait for Retry-After before retrying. 500, 502 and 504 and connection errors may be transient, so retry idempotent requests with exponential backoff and jitter and a small attempt limit. Non-idempotent POSTs that time out have an unknown outcome and need idempotency keys before retrying. Encode this once in Resilience4j or the client SDK so every team behaves the same.

Q8Solution ArchitectDesign

How would you standardise status codes across hundreds of APIs?

Standardise by making the right thing the default and the wrong thing visible. Publish an organisation-wide status catalogue in the API guidelines that maps common situations, validation, not found, conflict, stale ETag, rate limit, to specific codes with RFC 9457 Problem Details bodies. Ship it as a shared Spring Boot starter with a @RestControllerAdvice and exception types so services map errors identically. Lint OpenAPI specs in CI with Spectral rules, for example requiring 201 plus Location on creation and documented 4xx responses. Back it with contract tests and dashboards by status class per API, and align gateway behaviour, timeouts and SDK retry policies with the same catalogue.

Newsletter

Get new tutorials every week

New ArchitectMind tutorials and interview questions, once a week. No spam, unsubscribe anytime.

Almost done! Check your inbox and click the confirmation link to complete your subscription.
Keep going in Book 6

Databases, APIs & Integration

Volume I · Topic 1.1

Why Databases Exist

Database Fundamentals & Relational Data Modeling

Volume I · Topic 1.2

Database Management Systems

Database Fundamentals & Relational Data Modeling

Volume I · Topic 1.3

Database Architecture

Database Fundamentals & Relational Data Modeling

Volume II · Topic 1.1

Data Retrieval

SQL, Transactions, Concurrency & Database Performance

Volume II · Topic 1.2

Data Modification

SQL, Transactions, Concurrency & Database Performance

Volume II · Topic 1.3

Aggregation

SQL, Transactions, Concurrency & Database Performance

Volume III · Topic 1.1

JDBC Architecture

Java Persistence & Data Access Architecture

Volume III · Topic 1.2

PreparedStatement

Java Persistence & Data Access Architecture

Volume III · Topic 1.3

JDBC Transactions

Java Persistence & Data Access Architecture

Volume IV · Topic 1.1

Why NoSQL Exists

NoSQL, Caching & Polyglot Persistence

Volume IV · Topic 1.2

Categories

NoSQL, Caching & Polyglot Persistence

Volume IV · Topic 1.3

Trade-offs

NoSQL, Caching & Polyglot Persistence

Volume V · Topic 1.1

HTTP Architecture

API Design, HTTP & REST

Volume V · Topic 1.2

Methods

API Design, HTTP & REST

Volume VI · Topic 1.1

Gateway Responsibilities

API Management, GraphQL & Distributed API Design

Volume VI · Topic 1.2

Gateway Patterns

API Management, GraphQL & Distributed API Design

Volume VI · Topic 1.3

Gateway Risks

API Management, GraphQL & Distributed API Design

Volume VII · Topic 1.1

Why Systems Integrate

Enterprise Integration & Messaging

Volume VII · Topic 1.2

Integration Styles

Enterprise Integration & Messaging

Volume VII · Topic 1.3

Point-to-Point Integration

Enterprise Integration & Messaging

Volume VIII · Topic 1.1

Integration Boundaries

Integration Architecture & Data Consistency

Volume VIII · Topic 1.2

Synchronous vs Asynchronous

Integration Architecture & Data Consistency

Volume VIII · Topic 1.3

Coupling

Integration Architecture & Data Consistency