HTTP, HTTPS & Application Layer
A complete guide covering HTTP fundamentals for newcomers, a practical decision framework for choosing the right method and status code, and senior-level deep dives into protocol evolution, TLS internals, and production design patterns.
πΊοΈ How to Use This Documentβ
| You are... | Start here |
|---|---|
| New to HTTP | What Is HTTP? β Request Structure β Methods |
| Mid-level engineer | Decision Framework β Status Codes β Caching |
| Senior / system design | Protocol Evolution β TLS Deep Dive β Production Checklist |
What Is HTTP?β
Imagine HTTP as the language your browser and a web server use to talk to each other. When you type a URL and press Enter, your browser sends an HTTP request (like asking a question), and the server sends back an HTTP response (the answer). Every time you load a page, click a link, or submit a form, HTTP is happening under the hood.
HTTP (HyperText Transfer Protocol) is a stateless, request-response application protocol that powers the web. It operates over TCP (HTTP/1.x and HTTP/2) or QUIC (HTTP/3).
Stateless means every request is completely independent β the server remembers nothing about previous requests. This is why we need cookies, sessions, and tokens: they're workarounds for HTTP's statelessness, added at the application layer.
You (Browser) Server
β β
βββ GET /products HTTP/1.1 ββββΊβ "Give me the products page"
β β
ββββ 200 OK + HTML βββββββββββββ "Here it is"
β β
βββ POST /cart HTTP/1.1 βββββββΊβ "Add item to my cart"
β Cookie: session=abc123 β (cookie = workaround for statelessness)
β β
ββββ 201 Created βββββββββββββββ "Done, item added"
HTTP Request Structureβ
Every HTTP request has four parts:
POST /api/orders HTTP/1.1 β (1) Request Line: METHOD PATH VERSION
Host: api.example.com β (2) Headers: key-value metadata
Content-Type: application/json (required and optional info about the request)
Authorization: Bearer eyJhbGci...
Accept: application/json
Content-Length: 85
β (3) Blank line (signals end of headers)
{"userId": 42, "items": [...]} β (4) Body (optional β only for POST, PUT, PATCH)
Think of it like a physical letter:
- Request Line = the subject line ("Please process this order")
- Headers = envelope metadata (return address, content type, stamps)
- Body = the actual letter contents (the data you're sending)
HTTP Methods In Depthβ
HTTP methods (also called "verbs") tell the server what action to perform on a resource. Each method has a defined contract around safety and idempotency.
Understanding Safety and Idempotencyβ
These two properties are critical for designing resilient APIs and clients.
Safe means the request does not change server state. Safe methods can be cached, prefetched, and retried freely.
Idempotent means calling the same request N times has the same effect as calling it once. Idempotent requests can be safely retried on network failure without risk of duplicate side effects.
Example: DELETE /orders/42
First call: order exists β deleted β 200 OK
Second call: order gone β 404 Not Found
The server state is the same after both calls (order is gone).
β DELETE is idempotent even though the response code differs.
| Method | Safe | Idempotent | Has Body | Primary Use |
|---|---|---|---|---|
GET | β | β | β | Read / retrieve a resource |
HEAD | β | β | β | Read headers only (no body) |
OPTIONS | β | β | β | Discover allowed methods / CORS preflight |
DELETE | β | β | Rarely | Remove a resource |
PUT | β | β | β | Replace a resource entirely |
PATCH | β | β* | β | Partially update a resource |
POST | β | β | β | Create a resource, trigger an action |
CONNECT | β | β | β | Establish a TCP tunnel (proxy) |
*PATCH can be designed to be idempotent (e.g., SET field=value) or non-idempotent (e.g., INCREMENT field by 1). The HTTP spec leaves this to the implementation.
GET β Read a Resourceβ
Retrieves a representation of a resource. The most common method. Never use GET to modify state.
GET /api/products/42 HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer <token>
β No body. All parameters go in the URL or query string.
GET /api/products?category=electronics&sort=price&page=2 HTTP/1.1
Safe, idempotent β browsers can prefetch GET requests, CDNs can cache them, load balancers can retry on failure.
GET /api/orders/42/cancel is a design error. If a browser or CDN prefetches that URL, the order gets cancelled silently. Use POST /api/orders/42/cancel or DELETE /api/orders/42 instead.
@GetMapping("/products/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
return ResponseEntity.ok(productService.findById(id));
}
@GetMapping("/products")
public ResponseEntity<Page<Product>> listProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String category) {
return ResponseEntity.ok(productService.findAll(page, size, category));
}
Cacheability: GET responses can be cached by browsers, CDNs, and proxies. Control this with Cache-Control headers.
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600 β cache for 1 hour
ETag: "d8e8fca2dc0f896f" β fingerprint for conditional requests
POST β Create or Submitβ
Creates a new resource or triggers a server-side action. The response usually includes the newly created resource's URL in the Location header.
Not idempotent β retrying a POST may create duplicate resources. Always design APIs so clients don't need to retry blindly.
POST /api/orders HTTP/1.1
Content-Type: application/json
{"userId": 42, "items": [...], "total": 99.90}
β Response:
HTTP/1.1 201 Created
Location: /api/orders/1001 β URL of the new resource
Content-Type: application/json
{"orderId": 1001, "status": "pending"}
POST for actions (RPC-style):
POST is also the right method for actions that don't map cleanly to CRUD:
POST /api/orders/1001/cancel β cancel an order
POST /api/payments/1001/refund β refund a payment
POST /api/users/42/send-verification-email
POST /api/reports/generate
@PostMapping("/orders")
public ResponseEntity<Order> createOrder(@RequestBody @Valid CreateOrderRequest req) {
Order order = orderService.create(req);
URI location = URI.create("/api/orders/" + order.getId());
return ResponseEntity.created(location).body(order); // 201 Created + Location header
}
@PostMapping("/orders/{id}/cancel")
public ResponseEntity<Void> cancelOrder(@PathVariable Long id) {
orderService.cancel(id);
return ResponseEntity.noContent().build(); // 204 No Content
}
Idempotency key pattern β make POST idempotent when retries are needed:
POST /api/payments HTTP/1.1
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 β client-generated UUID
Server stores: if this key was seen before, return the original response.
Retry safety: client can retry on network failure without double-charging.
PUT β Replace Entirelyβ
Replaces a resource completely with the request body. The client sends the full representation β any fields not included are removed or reset to defaults.
Idempotent β calling PUT multiple times with the same body has the same result.
PUT /api/users/42 HTTP/1.1
Content-Type: application/json
{
"name": "Alice",
"email": "[email protected]",
"role": "admin",
"preferences": {"theme": "dark"}
}
β The entire user object is replaced. Every field must be included.
Missing fields are nulled/defaulted β not preserved.
@PutMapping("/users/{id}")
public ResponseEntity<User> replaceUser(
@PathVariable Long id,
@RequestBody @Valid UserRequest req) {
User user = userService.replace(id, req); // full replacement
return ResponseEntity.ok(user);
}
PUT can also create (upsert) if the client specifies the resource ID:
PUT /api/settings/user:42 β creates if not exists, replaces if it does
Use POST when the server assigns the ID (POST /orders β server creates order:1001).
Use PUT when the client assigns the ID (PUT /files/my-document.pdf β client-named resource).
PATCH β Partial Updateβ
Updates only the fields provided in the request body. Fields not mentioned are left unchanged. More efficient than PUT when only changing one or two fields of a large resource.
PATCH /api/users/42 HTTP/1.1
Content-Type: application/json
{"email": "[email protected]"}
β Only email is changed. name, role, preferences remain as-is.
(Compare: PUT would require sending all fields)
PATCH is not always idempotent. It depends on the operation:
Idempotent PATCH: {"status": "cancelled"} β set to cancelled (same result each time)
Non-idempotent PATCH: {"balance": {"$inc": 100}} β add 100 each time (different each call)
JSON Patch (RFC 6902) β a standardized patch format for complex operations:
PATCH /api/users/42 HTTP/1.1
Content-Type: application/json-patch+json
[
{"op": "replace", "path": "/email", "value": "[email protected]"},
{"op": "add", "path": "/tags/-", "value": "premium"},
{"op": "remove", "path": "/legacyField"}
]
@PatchMapping("/users/{id}")
public ResponseEntity<User> updateUser(
@PathVariable Long id,
@RequestBody Map<String, Object> updates) { // only fields to change
User user = userService.partialUpdate(id, updates);
return ResponseEntity.ok(user);
}
// Service: apply only non-null fields
public User partialUpdate(Long id, Map<String, Object> updates) {
User user = repo.findById(id).orElseThrow();
if (updates.containsKey("email")) user.setEmail((String) updates.get("email"));
if (updates.containsKey("name")) user.setName((String) updates.get("name"));
return repo.save(user);
}
DELETE β Remove a Resourceβ
Removes the specified resource. Returns 200 OK with a body, 204 No Content with no body, or 202 Accepted if deletion is async.
Idempotent β deleting an already-deleted resource returns 404, but the server state (resource absent) is the same.
DELETE /api/orders/1001 HTTP/1.1
Authorization: Bearer <token>
β HTTP/1.1 204 No Content (most common: success, no body)
β HTTP/1.1 404 Not Found (already deleted β still idempotent)
β HTTP/1.1 202 Accepted (async delete queued, not yet complete)
@DeleteMapping("/orders/{id}")
public ResponseEntity<Void> deleteOrder(@PathVariable Long id) {
orderService.delete(id);
return ResponseEntity.noContent().build(); // 204
}
// Soft delete β mark as deleted, don't actually remove from DB
@DeleteMapping("/users/{id}")
public ResponseEntity<Void> deactivateUser(@PathVariable Long id) {
userService.softDelete(id); // sets deletedAt, isActive=false
return ResponseEntity.noContent().build();
}
HEAD β Metadata Without Bodyβ
Identical to GET but the server returns only headers, no body. Used to check resource existence, size, or freshness without downloading the full content.
HEAD /api/files/report.pdf HTTP/1.1
β HTTP/1.1 200 OK
Content-Length: 2048576 β file is 2MB β client can decide whether to download
Content-Type: application/pdf
Last-Modified: Mon, 14 Mar 2026 09:00:00 GMT
ETag: "d8e8fca"
(no body)
Practical uses:
- Check if a file exists before downloading it
- Get the
Content-Lengthto show a progress bar - Validate an ETag before a conditional GET
- Health check:
HEAD /health(faster thanGET /healthsince no body is transferred)
@RequestMapping(value = "/files/{name}", method = RequestMethod.HEAD)
public ResponseEntity<Void> checkFile(@PathVariable String name) {
FileMetadata meta = fileService.getMetadata(name);
return ResponseEntity.ok()
.contentLength(meta.size())
.contentType(MediaType.APPLICATION_PDF)
.eTag(meta.etag())
.build();
}
OPTIONS β Discover Capabilitiesβ
Returns the HTTP methods and headers allowed for a resource. Primarily used for CORS preflight requests β the browser asks "can I make this cross-origin request?" before actually sending it.
OPTIONS /api/orders HTTP/1.1
Origin: https://frontend.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type
β HTTP/1.1 204 No Content
Allow: GET, POST, OPTIONS
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400 β cache preflight for 24h
Spring handles OPTIONS/CORS automatically when configured β you rarely implement @RequestMapping(method = OPTIONS) manually.
π§ Decision Framework: Which Method to Use?β
Flowchartβ
Quick-Reference Decision Matrixβ
| Scenario | Method | Status Code | Notes |
|---|---|---|---|
| Fetch a product | GET /products/42 | 200 | Cacheable |
| List with filters | GET /products?category=shoes | 200 | Params in query string |
| Create an order | POST /orders | 201 + Location | Not idempotent |
| Create with client ID | PUT /files/my-doc.pdf | 200 or 201 | Idempotent upsert |
| Replace user profile | PUT /users/42 | 200 | Send full object |
| Update just email | PATCH /users/42 | 200 | Send only {email: ...} |
| Cancel an order | POST /orders/42/cancel | 200 or 204 | Action, not a resource |
| Delete a record | DELETE /orders/42 | 204 | Idempotent |
| Check file size | HEAD /files/report.pdf | 200 | No body transferred |
| CORS preflight | OPTIONS /api/orders | 204 | Browser auto-sends |
PUT vs PATCH β When Does It Matter?β
Resource: User { name, email, role, preferences, address, billingInfo, ... }
Scenario: User changes only their email address.
PUT (wrong approach):
β Client must fetch the entire user object
β Change only email
β Send the entire object back (50+ fields over the wire)
β Race condition: if another field changed between GET and PUT, it's overwritten
PATCH (correct approach):
β Client sends only: {"email": "[email protected]"}
β Server changes only email, preserves all other fields
β No race condition on unrelated fields
β Much less bandwidth for large objects
Use PUT when you intentionally want to clear unset fields (e.g., a settings reset to defaults), or when the resource is small enough that sending the full object is trivial. PATCH requires careful merge logic on the server; PUT is simpler to implement correctly.
POST vs PUT for Creationβ
POST β server-assigned ID:
POST /api/orders
Body: {"items": [...]}
β Server creates order with ID=1001
β Returns: 201 Created, Location: /api/orders/1001
β Client doesn't know the ID until after the response
PUT β client-assigned ID:
PUT /api/files/quarterly-report-q1-2026.pdf
Body: <file bytes>
β Server stores the file at that exact path
β Idempotent: uploading the same file again replaces it, no duplicate
β Client owns the identifier
HTTP Response Status Codesβ
Status codes tell the client what happened on the server. Choosing the correct code is part of good API design β clients use them to decide how to react (retry, redirect, display error).
2xx β Successβ
| Code | Name | When to Use |
|---|---|---|
200 OK | Standard success | GET, PUT, PATCH, POST (when returning existing resource) |
201 Created | Resource created | POST that creates a new resource β include Location header |
202 Accepted | Async processing | Request accepted but not yet complete (async job queued) |
204 No Content | Success, no body | DELETE, POST actions that don't return data |
206 Partial Content | Range fulfilled | File download with Range header (video streaming) |
Use 204 when there is genuinely nothing meaningful to return (after a DELETE, after cancelling an order). Use 200 when you're returning the updated state of the resource. Never return 200 with an empty body when 204 is more correct β it confuses clients and breaks response parsing.
3xx β Redirectionβ
| Code | Name | When to Use |
|---|---|---|
301 Moved Permanently | Permanent redirect | Old URL is retired; update bookmarks and links |
302 Found | Temporary redirect | Temporarily moved; keep using the original URL |
304 Not Modified | Cached content valid | Conditional GET β client can use its cached copy |
307 Temporary Redirect | Redirect, keep method | Like 302 but preserves the HTTP method (POST stays POST) |
308 Permanent Redirect | Redirect, keep method | Like 301 but preserves the HTTP method |
301 and 302 historically allowed browsers to change POST to GET on redirect. 307 and 308 guarantee the method is preserved. For API redirects involving POST or PUT, always use 307 (temp) or 308 (perm).
4xx β Client Errorsβ
| Code | Name | When to Use |
|---|---|---|
400 Bad Request | Invalid input | Malformed JSON, missing required fields, validation failure |
401 Unauthorized | Not authenticated | No token, expired token, invalid credentials |
403 Forbidden | Not authorized | Authenticated but lacks permission |
404 Not Found | Resource absent | Resource doesn't exist (or you're hiding its existence) |
405 Method Not Allowed | Wrong method | DELETE /products when only GET is supported |
409 Conflict | State conflict | Duplicate create, version conflict (optimistic locking) |
410 Gone | Permanently removed | Resource existed but was permanently deleted |
415 Unsupported Media Type | Wrong content type | Sent XML when only JSON accepted |
422 Unprocessable Entity | Semantic validation failure | JSON is valid but business rules violated |
429 Too Many Requests | Rate limited | Include Retry-After header |
400 vs 422 β the nuance:
400 Bad Request: Body is malformed β can't even parse it
{"name": "Alice" β missing closing brace (invalid JSON)
422 Unprocessable: Syntactically valid but semantically wrong
{"age": -5} β valid JSON, but age can't be negative
{"startDate": "2026-01-01", "endDate": "2025-01-01"} β end before start
401 vs 403 β the most common confusion:
401 Unauthorized: "Who are you? I don't know you."
β User is not logged in, or their token is expired/invalid
β Response must include: WWW-Authenticate: Bearer realm="api"
β Client should: redirect to login, refresh token
403 Forbidden: "I know who you are, but you can't do this."
β User is logged in but doesn't have the required role/permission
β Client should: show "Access Denied" β logging in again won't help
5xx β Server Errorsβ
| Code | Name | When to Use |
|---|---|---|
500 Internal Server Error | Unhandled exception | Catch-all for unexpected errors β log and alert |
502 Bad Gateway | Upstream failure | Gateway received invalid response from upstream |
503 Service Unavailable | Service overloaded/down | Maintenance mode, circuit breaker tripped |
504 Gateway Timeout | Upstream timed out | Upstream didn't respond in time |
Always include Retry-After on 503 responses. This tells load balancers and clients when to retry, preventing a thundering herd of retries that worsens the outage.
HTTP/1.1 503 Service Unavailable
Retry-After: 30
Content-Type: application/json
{"error": "Service temporarily unavailable", "retryAfter": 30}
Important HTTP Headersβ
Request Headersβ
| Header | Purpose | Example |
|---|---|---|
Host | Target server (required in HTTP/1.1) | api.example.com |
Authorization | Auth credentials | Bearer <token> |
Content-Type | Request body format | application/json |
Accept | Acceptable response formats | application/json, text/html;q=0.9 |
Accept-Encoding | Compression algorithms supported | gzip, deflate, br |
Accept-Language | Language preference | en-US,en;q=0.9 |
Cache-Control | Caching directives from client | no-cache |
If-None-Match | Conditional GET β ETag check | "abc123" |
If-Modified-Since | Conditional GET β date check | Tue, 10 Mar 2026 00:00:00 GMT |
X-Forwarded-For | Client IP behind a proxy/LB | 203.0.113.5, 10.0.0.1 |
X-Request-ID | Correlation ID for distributed tracing | 550e8400-e29b-41d4-a716-446655440000 |
Idempotency-Key | Makes POST safely retryable | 550e8400-e29b-41d4-a716-446655440000 |
Origin | CORS β request's origin | https://app.example.com |
Response Headersβ
| Header | Purpose | Example |
|---|---|---|
Content-Type | Response body format | application/json; charset=utf-8 |
Content-Encoding | Compression applied | gzip |
Content-Length | Body size in bytes | 1024 |
Cache-Control | Caching policy | public, max-age=3600 |
ETag | Resource fingerprint | "d8e8fca2dc0f896f" |
Last-Modified | Last change timestamp | Mon, 14 Mar 2026 09:00:00 GMT |
Location | URL of new/redirected resource | /api/orders/1001 |
Retry-After | Seconds until retry allowed | 30 |
Strict-Transport-Security | Force HTTPS (HSTS) | max-age=31536000; includeSubDomains |
X-Content-Type-Options | Prevent MIME sniffing | nosniff |
X-Frame-Options | Prevent clickjacking | DENY |
Content-Security-Policy | XSS / injection prevention | default-src 'self' |
Access-Control-Allow-Origin | CORS allowlist | https://app.example.com |
X-Request-ID | Echo correlation ID for tracing | 550e8400-e29b-41d4-a716-446655440000 |
Security Headers β Why They Matterβ
// Spring Security: add security headers to all responses
@Configuration
public class SecurityHeadersConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers(headers -> headers
// Prevent clickjacking: refuse to render in iframes
.frameOptions(frame -> frame.deny())
// Prevent MIME-type sniffing (browser trusts Content-Type header)
.contentTypeOptions(Customizer.withDefaults())
// Force HTTPS for 1 year, including subdomains
.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(31536000)
.includeSubDomains(true)
.preload(true))
// Restrict resource loading sources (prevents XSS)
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self' https://cdn.trusted.com"))
);
return http.build();
}
}
HTTP Cachingβ
HTTP caching reduces bandwidth, reduces server load, and improves perceived performance. It works by allowing clients (browsers) and intermediaries (CDNs, proxies) to store and reuse responses.
Cache-Control Directivesβ
Cache-Control: max-age=3600 # cache for 1 hour (client + CDN)
Cache-Control: s-maxage=86400 # CDN TTL β overrides max-age for proxies
Cache-Control: public # any cache (browser, CDN) may store
Cache-Control: private # only browser may cache (not CDN)
Cache-Control: no-cache # store locally, but revalidate before use
Cache-Control: no-store # never cache β not in browser or CDN
Cache-Control: immutable # content will never change β skip revalidation
Cache-Control: stale-while-revalidate=60 # serve stale for 60s while refreshing async
Cache-Control: must-revalidate # expired copies must not be served, ever
Choosing Cache-Control by content type:
| Content Type | Recommended Directive | Reason |
|---|---|---|
| Static assets (JS, CSS with hash) | public, max-age=31536000, immutable | Content hash in URL = safe to cache forever |
| API GET responses | private, max-age=300 or no-cache | May be user-specific |
| Public data (product catalog) | public, max-age=3600, s-maxage=86400 | CDN caches longer than browser |
| Authenticated responses | private, no-cache | Don't cache user-specific data in CDN |
| Sensitive data (banking) | no-store | Never cache anywhere |
| Real-time data | no-store or max-age=0 | Must always be fresh |
ETags and Conditional Requestsβ
ETags let clients validate whether their cached copy is still current without downloading the full response body if it hasn't changed.
@GetMapping("/products/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id,
@RequestHeader(value = "If-None-Match",
required = false) String ifNoneMatch) {
Product product = productService.findById(id);
// Generate ETag from content hash or version
String etag = '"' + DigestUtils.md5DigestAsHex(
objectMapper.writeValueAsBytes(product)) + '"';
// If client's cached version is still valid, return 304 (no body)
if (etag.equals(ifNoneMatch)) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED)
.eTag(etag)
.build();
}
return ResponseEntity.ok()
.eTag(etag)
.cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES))
.body(product);
}
Protocol Evolution: HTTP/1.1 β HTTP/2 β HTTP/3β
HTTP/1.1 β The Baseline (1997)β
Key innovation over HTTP/1.0: persistent connections (Connection: keep-alive). Instead of opening a new TCP connection per request, the connection is reused.
The problem that remained β Head-of-Line (HoL) Blocking:
HTTP/1.1 with pipelining (not widely used):
Connection: [req1]ββββββββββ[resp1 (slow)]ββ[req2]ββ[resp2]ββ[req3]ββ[resp3]
β
Slow response 1 blocks req2 and req3
Even if they would have responded instantly
The workaround: browsers open 6 parallel TCP connections per domain β which is wasteful (6Γ handshakes, 6Γ TLS negotiations, 6Γ congestion windows).
HTTP/2 β Multiplexing (2015)β
HTTP/2's core innovation: multiplexing β many request/response pairs share a single TCP connection as independent "streams".
HTTP/1.1: 6 parallel TCP connections, 1 request per connection
TCP1: [GET /js/app.js ββββββββββββββββ 120ms ββββ]
TCP2: [GET /css/style.css βββ 40ms β]
TCP3: [GET /api/user βββββ 60ms ββ]
(3 TCP handshakes, 3 TLS negotiations)
HTTP/2: 1 TCP connection, all requests multiplexed
Stream 1: [GET /js/app.js ββββββββββββ 120ms ββ]
Stream 2: [GET /css/style.css β 40ms β]
Stream 3: [GET /api/user ββββ 60ms ββ]
(1 TCP handshake, 1 TLS negotiation)
HTTP/2 features summary:
| Feature | Description | Benefit |
|---|---|---|
| Multiplexing | Multiple streams on one connection | No parallel connection limit |
| Header Compression (HPACK) | Compresses repeated headers | 85-95% reduction in header size |
| Server Push | Server sends resources preemptively | Eliminate round-trips for critical assets |
| Stream Prioritization | Weight streams by importance | Critical resources delivered first |
| Binary Framing | Compact binary over text | More efficient parsing |
HTTP/2's remaining problem β TCP-level HoL blocking:
TCP treats the connection as a single ordered byte stream.
If one TCP packet is lost, all streams wait for retransmission,
even streams whose packets arrived fine.
Stream 1 (JS): [pkt1]ββ[pkt2]ββ[LOST]ββ[pkt4]
Stream 2 (CSS): [pkt1]ββ[pkt2]ββ[pkt3] β ready, but blocked waiting for Stream 1's retransmit
Stream 3 (API): [pkt1]ββ[pkt2]ββ[pkt3] β same
HTTP/2 eliminated application-level HoL; TCP-level HoL remains.
HTTP/3 β QUIC (2022)β
HTTP/3 replaces TCP with QUIC β a new transport protocol built on UDP that provides reliability per-stream.
HTTP/3 / QUIC key improvements:
| Feature | HTTP/2 (TCP) | HTTP/3 (QUIC) |
|---|---|---|
| HoL Blocking | TCP-level HoL blocking | Per-stream β lost packet only blocks its own stream |
| Connection Setup | 1 RTT TCP + 1 RTT TLS = 2 RTT | 1 RTT first time, 0-RTT resumption |
| TLS | Separate TLS layer | Built into QUIC (always encrypted) |
| Connection Migration | IP change = new connection | Connection persists across IP changes |
| Congestion Control | Per-connection | Per-stream β more granular |
Connection setup latency comparison:
HTTP/1.1 & HTTP/2:
β TCP SYN (client β server)
β TCP SYN-ACK (server β client) = 1 RTT (TCP handshake)
β TCP ACK + ClientHello
β ServerHello + Certificate = 1 RTT (TLS 1.3)
β First HTTP request = 3rd RTT
HTTP/3 (QUIC):
First connection:
β QUIC Initial (includes TLS ClientHello)
β QUIC Handshake (TLS ServerHello + cert) = 1 RTT
β HTTP request = 2nd RTT
Resumed connection (0-RTT):
β QUIC + HTTP request (in same packet) = 0 RTT! β data in first packet
Connection migration β especially impactful on mobile:
User on WiFi:
[Phone] ββ [Server] via IP: 192.168.1.5
User walks outside, switches to 4G:
HTTP/2: TCP connection broken β new connection β new TLS β re-authentication
HTTP/3: QUIC connection migrates β same connection ID β no interruption
HTTPS & TLS Deep Diveβ
HTTPS = HTTP + TLS (Transport Layer Security). TLS provides: encryption (eavesdroppers can't read traffic), authentication (you're talking to the real server), and integrity (data wasn't tampered with in transit).
TLS 1.3 Handshakeβ
TLS 1.3 vs 1.2:
| Aspect | TLS 1.2 | TLS 1.3 |
|---|---|---|
| Handshake RTTs | 2 RTT | 1 RTT (0-RTT for resumption) |
| Cipher suites | Many, including weak ones | Only strong AEAD ciphers |
| Key exchange | RSA (no forward secrecy) + ECDHE | ECDHE only (mandatory forward secrecy) |
| Handshake encryption | Partially plaintext | Most handshake messages encrypted |
| Deprecated algorithms | RC4, MD5, SHA-1 allowed | Removed entirely |
Forward Secrecy β why it matters:
Without Forward Secrecy (RSA key exchange):
Attacker records encrypted traffic today.
Years later, obtains server's private key (breach, subpoena, etc.)
β Decrypts all previously recorded traffic retroactively.
With Forward Secrecy (ECDHE β mandatory in TLS 1.3):
New ephemeral key pair generated for each session.
Session key discarded after session ends.
β Past sessions cannot be decrypted even with the server's private key.
Certificate Chain of Trustβ
Root CA Certificate
(pre-installed in all browsers/OS β the "trust anchor")
β
βββ Intermediate CA Certificate
(signed by Root CA β adds a layer of security)
β
βββ Server Certificate (e.g., api.example.com)
(signed by Intermediate CA β what the server presents)
Browsers verify: "Is this server certificate signed by an Intermediate CA that is signed by a Root CA that I trust?"
Spring Boot TLS Configurationβ
# application.yml β production TLS setup
server:
port: 8443
ssl:
enabled: true
key-store: classpath:keystore.p12
key-store-password: ${SSL_KEYSTORE_PASSWORD} # from env var, never hardcoded
key-store-type: PKCS12
key-alias: myserver
protocol: TLS
enabled-protocols: TLSv1.3,TLSv1.2 # TLS 1.0 and 1.1 disabled
ciphers: >
TLS_AES_128_GCM_SHA256,
TLS_AES_256_GCM_SHA384,
TLS_CHACHA20_POLY1305_SHA256,
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
// Redirect HTTP β HTTPS (never serve plaintext in production)
@Configuration
public class HttpsRedirectConfig {
@Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
@Override
protected void postProcessContext(Context context) {
SecurityConstraint sc = new SecurityConstraint();
sc.setUserConstraint("CONFIDENTIAL"); // forces HTTPS
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/*");
sc.addCollection(collection);
context.addConstraint(sc);
}
};
tomcat.addAdditionalTomcatConnectors(httpToHttpsRedirectConnector());
return tomcat;
}
private Connector httpToHttpsRedirectConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setScheme("http");
connector.setPort(8080);
connector.setSecure(false);
connector.setRedirectPort(8443);
return connector;
}
}
CORS β Cross-Origin Resource Sharingβ
Browsers have a Same-Origin Policy: JavaScript on https://app.example.com is blocked from making requests to https://api.other.com. This prevents malicious websites from silently reading your Gmail or bank data using your logged-in session. CORS is the mechanism servers use to selectively lift this restriction.
Origin = scheme + host + port
https://app.example.com:443 β one origin
https://api.example.com:443 β different origin (different subdomain)
http://app.example.com:80 β different origin (different scheme + port)
CORS Preflight Flowβ
CORS is enforced by browsers, not servers. Server-to-server API calls (curl, Postman, mobile apps, backend services) are never subject to CORS. If someone claims your API has a CORS bug but they found it with Postman β it's not a CORS bug.
// Global CORS configuration (Spring MVC)
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins(
"https://app.example.com",
"https://admin.example.com"
)
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
.allowedHeaders("Authorization", "Content-Type", "X-Request-ID")
.exposedHeaders("X-Request-ID", "X-RateLimit-Remaining") // headers JS can read
.allowCredentials(true) // allow cookies/auth headers
.maxAge(86400); // cache preflight for 24h
}
}
// Per-controller override
@CrossOrigin(origins = "https://partner.example.com")
@RestController
@RequestMapping("/api/public")
public class PublicApiController { ... }
Production Readiness Checklistβ
HTTP Methods & API Designβ
- GET endpoints are truly read-only and safe to cache / prefetch
- POST returns
201 Created+Locationheader when creating resources - PUT is used for full replacement (not partial updates)
- PATCH is used for partial updates (not full replacement)
- DELETE returns
204 No Content(not 200 with empty body) - Actions use POST (
/orders/42/cancel), not GET - Idempotency keys implemented for critical POST endpoints (payments, emails)
Status Codesβ
- 401 returned for unauthenticated requests (with
WWW-Authenticateheader) - 403 returned for authenticated-but-unauthorized requests
- 400 vs 422 used correctly (malformed vs semantically invalid)
- 503 includes
Retry-Afterheader - 429 includes
Retry-Afterheader and rate limit headers
Securityβ
- TLS 1.3 enabled; TLS 1.0 and 1.1 disabled
- HTTP redirects to HTTPS (301 redirect)
- HSTS configured with
includeSubDomainsandpreload -
X-Content-Type-Options: nosniffon all responses -
X-Frame-Options: DENYor CSPframe-ancestors: 'none' - Content-Security-Policy header configured
- CORS allowlist uses explicit origins (no
*with credentials) -
X-Forwarded-Foronly trusted from known proxies/load balancers
Cachingβ
- Static assets have
Cache-Control: public, max-age=31536000, immutable - Authenticated API responses have
Cache-Control: privateorno-store - Sensitive data (financial, PII) has
Cache-Control: no-store - ETags implemented for expensive GET responses
- CDN caching configured with appropriate
s-maxage
Observabilityβ
-
X-Request-IDheader echoed on every response (distributed tracing) - Request method and path included in access logs
- 4xx and 5xx rates alerted separately (4xx = client issues, 5xx = your issues)
- Response time p50/p95/p99 tracked per endpoint
π― Interview Questionsβ
Foundationalβ
Q: What is the difference between GET and POST?
GET retrieves a resource β it is safe (no side effects), idempotent, and the parameters go in the URL. It should never modify state. POST submits data to the server to create a resource or trigger an action β it is neither safe nor idempotent, and data goes in the request body. GET responses are cacheable; POST responses generally are not. The key distinction: GET reads, POST writes.
Q: What does idempotent mean, and which HTTP methods are idempotent?
An operation is idempotent if calling it N times produces the same server state as calling it once. GET, HEAD, OPTIONS, PUT, and DELETE are idempotent. POST and PATCH are not (by default). Idempotency matters for retry logic: if a network request fails, you can safely retry an idempotent method without risk of duplicating side effects. For example, a client can safely retry
DELETE /orders/42β if the order is already deleted, the state remains "order deleted".
Q: When would you use PUT vs PATCH?
Use PUT for full replacement β the client must send the entire resource representation, and any fields omitted are cleared. Use PATCH for partial updates β the client sends only the fields to change. PATCH is more efficient (less bandwidth) and avoids race conditions where a concurrent write to an unrelated field gets overwritten. Use PUT when the full replacement behavior is intentional β such as a settings reset.
Q: What is the difference between 401 and 403?
401 Unauthorized means the request lacks valid authentication β the user is not identified (expired token, no credentials). The response should include
WWW-Authenticateto tell the client how to authenticate. The fix is for the client to log in or refresh their token. 403 Forbidden means the user is identified but doesn't have permission to perform the action. Logging in again will not help. The most common mistake is returning 403 when the user is simply not logged in β that should be 401.
Intermediateβ
Q: What is the difference between HTTP/1.1, HTTP/2, and HTTP/3?
HTTP/1.1 uses persistent connections (keep-alive) but suffers from application-level head-of-line blocking β one slow response blocks subsequent ones on the connection, so browsers work around this by opening 6 parallel TCP connections per domain. HTTP/2 introduces multiplexing β many concurrent request/response streams over one TCP connection β along with header compression (HPACK) and server push. However, it still suffers TCP-level HoL blocking: a single lost TCP packet stalls all streams. HTTP/3 uses QUIC over UDP, eliminating TCP-level HoL blocking (each stream has independent reliability), and provides 0-RTT connection resumption and connection migration (helpful on mobile).
Q: What is the difference between Cache-Control: no-cache and no-store?
no-cachemeans: you may store a cached copy, but you must revalidate with the server before using it (sendsIf-None-Match/If-Modified-Since). If the server responds with304 Not Modified, the cached copy is used β saving bandwidth.no-storemeans: never store a copy anywhere β not in the browser cache, not in CDNs, not in proxies. Useno-storefor genuinely sensitive data (financial transactions, authentication responses) where even having a cached copy on disk is unacceptable.
Q: What is an ETag and how does it enable conditional caching?
An ETag is a fingerprint of a resource's content (a hash or version number). The server returns
ETag: "abc123"with a response. On the next request, the client sendsIf-None-Match: "abc123". If the content hasn't changed, the server returns304 Not Modifiedwith no body β the client uses its cached copy, saving the full response body transfer. ETags enable bandwidth-efficient cache validation without relying on timestamps (which can be unreliable due to clock skew and second-level precision).
Senior / System Designβ
Q: How does TLS 1.3 improve upon TLS 1.2, and what is forward secrecy?
TLS 1.3 reduces handshake RTTs from 2 to 1 (with 0-RTT resumption), removes all weak cipher suites (RSA key exchange, RC4, MD5, SHA-1), and mandates ECDHE key exchange for every session, which provides forward secrecy. Forward secrecy means each session uses an ephemeral key pair that is discarded after the session ends. Even if an attacker records all TLS traffic today and later obtains the server's private key (via breach or subpoena), they cannot retroactively decrypt past sessions β because the ephemeral session keys no longer exist.
Q: Why is CORS only a browser concern, and what are the security implications?
CORS is enforced by browsers as part of the Same-Origin Policy β a browser security feature to prevent malicious websites from making authenticated cross-origin requests on behalf of users. Non-browser HTTP clients (curl, Postman, backend services, mobile apps) do not enforce CORS. This has two implications: (1) A CORS misconfiguration is only exploitable via a browser context β typically CSRF-style attacks where a malicious page makes requests using the victim's cookies. (2) Testing CORS with Postman does not accurately represent browser behavior β you must test with an actual browser or a tool that sends proper preflight requests.
Q: How would you design an API to make POST requests safely retryable?
POST is not idempotent by default, but you can make it idempotent using the Idempotency Key pattern: the client generates a unique UUID for each logical operation and sends it as
Idempotency-Key: <uuid>in the request. The server stores the key and the result in a short-lived store (Redis with TTL). On retry, if the server sees a key it has already processed, it returns the original response without re-executing the operation. This pattern is essential for payment processing, email sending, and any operation where duplicate execution causes real-world harm. Stripe, Adyen, and most payment APIs require idempotency keys on all write operations.
Q: Explain HTTP/2 server push and why it fell out of favor.
Server push lets the server proactively send resources (CSS, JS, fonts) to the client before it requests them β reducing round-trips for critical assets. In theory, when a browser requests
index.html, the server can simultaneously pushapp.jsandstyle.cssbefore the browser even parses the HTML. In practice, server push had significant problems: it bypassed the browser cache (the server couldn't know if the browser already had the resource cached), it competed with other streams for bandwidth, and it was difficult to implement correctly without over-pushing. HTTP/3 / the HTTP Working Group has effectively deprecated server push in favor of the103 Early Hintsresponse code and theLink: rel=preloadheader, which let the browser decide whether to fetch the resource based on its own cache state.
See Alsoβ
- Rate Limiting β 429 status codes,
Retry-Afterheader, throttling strategies - Caching Strategies β
Cache-Control, ETags, CDN caching in depth - API Design β REST resource naming, versioning, error response schemas
- Security Patterns β CSRF, XSS, CSP, auth header patterns
- Distributed Systems β Connection pooling, circuit breaking, timeouts