Definition
- 1xx Informational – request received, continue processing; 100 Continue is used for large uploads.
- 2xx Success – request succeeded; 200 OK, 201 Created, 204 No Content, 202 Accepted for async.
- 3xx Redirection – the resource moved; 301 vs 308 differ in whether the method is preserved; 304 Not Modified must have no body.
- 4xx Client Error – the client made a mistake; 400 malformed vs 422 semantically invalid, 401 not authenticated vs 403 not authorised, 405 wrong verb vs 501 not implemented.
- 5xx Server Error – the server failed; 502 bad upstream response, 503 temporarily down, 504 upstream timeout.
- 429 vs 503 – 429 means this client is rate-limited; 503 means the whole service is overloaded.
Think of the National Bank of India clerk's stamps with fine print. A 400 stamp says "your form is unreadable." A 422 stamp says "your form is readable but you asked for something not allowed." A 503 stamp says "the counter is closed, come back soon." A 504 stamp says "the counter sent your form to the back office and the back office never replied." These subtle differences tell Rajesh's app exactly what to do next: fix the form, wait and retry, or give up.
Understanding the Concepts
What are the five status code classes?
HTTP status codes are three-digit numbers grouped into five classes by their first digit. The class tells you who is responsible, and the exact code tells the client what to do next.
| Class | Meaning | Examples |
|---|---|---|
| 1xx | Informational: request received, continue | 100 Continue |
| 2xx | Success: request processed successfully | 200, 201, 202, 204 |
| 3xx | Redirection: the resource moved | 301, 302, 304, 307, 308 |
| 4xx | Client Error: the client made a mistake | 400, 401, 403, 404, 405, 409, 422, 429 |
| 5xx | Server Error: the server failed | 500, 502, 503, 504 |
// Quick reference in a Spring controller:
// return ResponseEntity.ok(body); // 200
// return ResponseEntity.created(location).body(body); // 201
// return ResponseEntity.noContent().build(); // 204
// return ResponseEntity.notFound().build(); // 404
// return ResponseEntity.status(HttpStatus.CONFLICT).body(body); // 409
What is 400 vs 422?
400 Bad Request means the request is malformed: the server cannot parse it. Invalid JSON, a missing required field, or a wrong data type. 422 Unprocessable Entity means the request is well-formed and parseable, but the content fails semantic or business-rule validation: a transfer amount exceeding the daily limit, or insufficient funds.
package com.bank.exception;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.MethodArgumentNotValidException;
@RestControllerAdvice
public class StatusCodeHandler {
// 400: malformed request – validation annotations failed.
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiError> handleBadRequest(MethodArgumentNotValidException e) {
return ResponseEntity.badRequest()
.body(new ApiError("BAD_REQUEST", "Invalid input: " + e.getMessage()));
}
// 422: well-formed but business rule violated.
@ExceptionHandler(InsufficientFundsException.class)
public ResponseEntity<ApiError> handleUnprocessable(InsufficientFundsException e) {
return ResponseEntity.unprocessableEntity()
.body(new ApiError("INSUFFICIENT_FUNDS", e.getMessage()));
}
}
What is 204 vs 200 for PUT?
If a PUT updates a resource and you want to return the updated representation, use 200 with a body. If the update succeeded but you choose not to echo the resource, use 204 No Content. Both are valid; the choice is whether the client needs the updated resource returned.
// 200 with body: client receives the updated resource.
@PutMapping("/beneficiaries/{id}")
public ResponseEntity<BeneficiaryResponse> replace(@PathVariable String id,
@RequestBody BeneficiaryRequest body) {
BeneficiaryResponse updated = beneficiaryService.replace(id, body);
return ResponseEntity.ok(updated); // 200 + updated body
}
// 204 without body: update succeeded, no echo.
@PutMapping("/beneficiaries/{id}")
public ResponseEntity<Void> replaceNoEcho(@PathVariable String id,
@RequestBody BeneficiaryRequest body) {
beneficiaryService.replace(id, body);
return ResponseEntity.noContent().build(); // 204 No Content
}
What is 301 vs 308 and why avoid 302 for POST redirects?
301 Moved Permanently historically allowed clients to change a POST into a GET when following the redirect, losing the request body. 308 Permanent Redirect guarantees the method and body are preserved. For a permanently moved POST endpoint, use 308, not 301.
The same issue applies to 302 Found vs 307 Temporary Redirect. 302 may turn POST into GET; 307 preserves the method. Always use 307 or 308 for redirects where method preservation matters.
// Wrong: 301 may cause POST to become GET, losing the transfer body.
return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY)
.header("Location", "/api/v2/transfers")
.build();
// Correct: 308 preserves POST method and body.
return ResponseEntity.status(HttpStatus.PERMANENT_REDIRECT)
.header("Location", "/api/v2/transfers")
.build();
What is 401 vs 403?
401 Unauthorized means the client is not authenticated: no valid credentials were provided. The response should include a WWW-Authenticate header. 403 Forbidden means the client is authenticated but does not have permission. Retrying with the same credentials will not help.
// 401: not authenticated – client should log in.
if (token == null || !tokenService.isValid(token)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.header("WWW-Authenticate", "Bearer")
.body(new ApiError("AUTH_REQUIRED", "Authentication required"));
}
// 403: authenticated but not permitted.
if (!user.hasRole("ADMIN")) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new ApiError("FORBIDDEN", "You do not have permission"));
}
What is 405 vs 501?
405 Method Not Allowed means the HTTP method is not supported on this specific resource. The server should return an Allow header listing valid methods. 501 Not Implemented means the server does not support the method at all for any resource.
// 405 with Allow header listing valid methods.
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
.allow(HttpMethod.GET, HttpMethod.PUT)
.body(new ApiError("METHOD_NOT_ALLOWED", "Use GET or PUT on this resource"));
What is 502 vs 503 vs 504?
These three server errors distinguish different failure modes. 502 Bad Gateway means a gateway got an invalid response from an upstream server. 503 Service Unavailable means the server itself is temporarily down, often with a Retry-After header. 504 Gateway Timeout means an upstream server did not respond in time.
| Code | Meaning | Action |
|---|---|---|
| 502 | Bad Gateway: upstream sent an invalid response | Fix the upstream |
| 503 | Service Unavailable: server temporarily down | Retry after delay |
| 504 | Gateway Timeout: upstream did not respond | Check upstream latency |
// 503 with Retry-After header.
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.header("Retry-After", "30")
.body(new ApiError("SERVICE_DOWN", "Try again in 30 seconds"));
What is 429 vs 503?
429 Too Many Requests means this specific client exceeded its rate limit. 503 means the whole service is overloaded regardless of which client. Both may include Retry-After, but 429 targets one client while 503 affects everyone.
// 429: rate limit for one client.
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
.header("Retry-After", "60")
.body(new ApiError("RATE_LIMITED", "Rate limit exceeded, retry in 60s"));
// 503: entire service overloaded.
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.header("Retry-After", "30")
.body(new ApiError("SERVICE_DOWN", "Service temporarily unavailable"));
Can 304 have a body?
304 Not Modified must not have a body. Its entire purpose is to tell the client to reuse its cached copy. Sending a body would defeat the point and violates the HTTP specification.
// 304 with no body: correct.
return ResponseEntity.status(HttpStatus.NOT_MODIFIED)
.eTag("\"v7\"")
.build(); // No body!
What are the risks of always returning 200?
If every response is 200 with a success flag in the body, caches may cache errors, browsers skip error handling, monitoring counts failures as successes, retry logic cannot distinguish a transient 503 from a permanent 400, and the entire value of HTTP semantics collapses.
// The cost of 200-for-everything:
// 1. Monitoring dashboard: all green during an outage.
// 2. Client error path: never triggered, failures are silently ignored.
// 3. Retry logic: never retries, because 200 means success.
// 4. Cache: may cache an error response as if it were valid data.
// 5. SLO alerts: never fire, because error rate appears 0%.
What should a senior engineer know about status codes?
A senior engineer knows these distinctions are not pedantry. A proxy automatically retries 503 and 429 after a delay but never retries 400 or 422. A browser following a 308 keeps the POST body but a 301 may drop it. A CDN serves a 304 from cache. Each precise code triggers correct automatic behaviour across infrastructure you do not control.
Visual Overview
┌──────────────────────────────────────────────────────┐ │ HTTP Status Code Decision Tree │ ├──────────────────────────────────────────────────────┤ │ │ │ Request processed? │ │ │ │ │ │ Yes No │ │ │ │ │ │ ▼ ▼ │ │ 2xx Success Whose fault? │ │ ┌─────────┐ │ │ │ │ │ 200 OK │ Client Server │ │ │ 201 Crtd│ │ │ │ │ │ 204 None│ ▼ ▼ │ │ │ 202 Accp│ 4xx Error 5xx Error │ │ └─────────┘ ┌─────────┐ ┌─────────┐ │ │ │ 400 Bad │ │ 500 Int │ │ │ │ 401 Auth │ │ 502 Bad │ │ │ │ 403 Perm │ │ 503 Down│ │ │ │ 404 None │ │ 504 Tout│ │ │ │ 409 Conf │ └─────────┘ │ │ │ 422 Biz │ │ │ │ 429 Rate │ │ │ └─────────┘ │ │ │ │ Tricky Pairs: │ │ 400 (malformed) vs 422 (business rule) │ │ 401 (not authed) vs 403 (not allowed) │ │ 301 (may change method) vs 308 (preserves method) │ │ 429 (one client) vs 503 (whole service) │ │ 502 (bad upstream) vs 504 (upstream timeout) │ └──────────────────────────────────────────────────────┘
Key Takeaway
HTTP status codes come in five classes: 1xx informational, 2xx success, 3xx redirection, 4xx client error, and 5xx server error. The class tells you who is responsible, but the precise code within the class tells the client exactly what to do next, and that precision is what makes caches, proxies, and monitoring behave correctly.
The tricky cases are where real understanding shows. For 400 vs 422, a 400 means the request was malformed, while a 422 means the request was well formed but violated a business rule. For 401 vs 403, a 401 means "not authenticated, please log in," while a 403 means "authenticated but not permitted." For 301 vs 308, the key is method preservation: a 301 may cause a client to turn a POST into a GET and lose the body, so a permanently moved POST endpoint must use 308.
For the server errors, 502 means a gateway got a bad response from upstream, 503 means the service itself is temporarily down with a Retry-After header, and 504 means an upstream server timed out. The related pair 429 vs 503 distinguishes one client being rate-limited from the whole service being overloaded. And 304 Not Modified must never carry a body, because its purpose is to tell the client to reuse its cached copy.
At National Bank of India this precision mattered concretely. When the team moved the payments endpoint and returned 301, a partner's integration resent a POST as a GET and silently dropped the transfer body. Switching to 308 fixed it. The biggest lesson was never returning 200 for everything with a success flag in the body, because that blinds caches, stops browsers from handling errors, hides failures from monitoring, and makes retry logic impossible. The precise code is an instruction to every piece of infrastructure between the client and the server.
Remember one sentence: the exact status code instructs the entire HTTP ecosystem what happened and what to do next, so choosing precisely between 400 and 422, 401 and 403, 301 and 308, and 502, 503, and 504 is a correctness requirement, not pedantry.
---