Request/Response ā REST & Synchronous Integration
Master synchronous integration with REST: HTTP semantics, resource design, status codes, versioning, idempotency, resilience patterns, OpenAPI contracts, and knowing exactly when synchronous is the right choice.
Request/Response is the oldest and most intuitive integration pattern: one system asks a question, another system answers it. In the modern web this means REST over HTTP ā but doing it well involves far more than hitting an endpoint. This lesson covers the full depth of synchronous integration: the protocol semantics that make it reliable, the design decisions that make it maintainable, and the resilience techniques that make it survive real-world conditions.
What Request/Response Actually Means
Request/Response is a synchronous, two-way communication pattern:
Client Server
ā ā
āāāāā HTTP Request āāāāāāāāāāāāāŗā
ā ā (client blocks here)
āāāāā HTTP Response āāāāāāāāāāāāā
ā āThe defining characteristic is temporal coupling: the client must wait while the server processes the request. Neither side can proceed independently ā the client is blocked until the response arrives or the connection times out.
This coupling is a deliberate trade-off. You accept it because you genuinely need the answer before you can continue.
When Synchronous Request/Response Is the Right Choice
Use it when:
- The caller cannot proceed without the result (e.g., validate a payment before confirming an order)
- Latency is critical and the operation completes in milliseconds to low seconds
- Immediate feedback is required (user-facing operations, real-time queries)
- The operation is stateless and idempotent ā the same request can safely be retried
Do not use it when:
- The operation takes more than a few seconds (use async + callback instead)
- The caller can continue without the result (use fire-and-forget messaging)
- High availability is more important than immediacy (a downed server blocks all callers)
- You need to fan out to multiple receivers (pub/sub scales better)
HTTP as an Integration Protocol
REST (Representational State Transfer) is an architectural style, not a protocol. It uses HTTP as its transfer protocol and treats everything as a resource addressable by a URL.
The HTTP Verbs and Their Contracts
Understanding HTTP verb semantics is not optional ā it determines how caches, proxies, load balancers, and retry logic behave:
| Verb | Meaning | Safe? | Idempotent? | Has Body? |
|------|---------|-------|-------------|-----------|
| GET | Retrieve a resource | Yes | Yes | No |
| POST | Create a resource or trigger an action | No | No | Yes |
| PUT | Replace a resource entirely | No | Yes | Yes |
| PATCH | Partially update a resource | No | No* | Yes |
| DELETE | Remove a resource | No | Yes | Optional |
| HEAD | Retrieve headers only (no body) | Yes | Yes | No |
Safe means the operation has no side effects ā a GET should never modify state.
Idempotent means calling it N times has the same effect as calling it once ā critical for safe retries.
HTTP Status Codes You Must Use Correctly
Misused status codes break client error handling and caching:
2xx ā Success
200 OK Standard success with body
201 Created Resource was created; include Location header
202 Accepted Request accepted; processing is async
204 No Content Success with no response body (DELETE, PUT)
3xx ā Redirection
301 Moved Permanently Resource has a new permanent URL
304 Not Modified Cached version is still valid (ETags)
4xx ā Client Errors (caller's fault ā do not retry without changing the request)
400 Bad Request Malformed request syntax or invalid data
401 Unauthorized Not authenticated
403 Forbidden Authenticated but not authorised
404 Not Found Resource does not exist
409 Conflict State conflict (e.g., duplicate resource)
410 Gone Resource permanently deleted
422 Unprocessable Entity Validation failed (well-formed but semantically invalid)
429 Too Many Requests Rate limit exceeded; includes Retry-After header
5xx ā Server Errors (server's fault ā may be worth retrying)
500 Internal Server Error Unexpected server failure
502 Bad Gateway Upstream service returned invalid response
503 Service Unavailable Server overloaded or down; includes Retry-After
504 Gateway Timeout Upstream service timed outRule: 4xx means the client must change something before retrying. 5xx (and 429) means the client can retry the same request after waiting.
RESTful Resource Design
Resource Naming
Resources are nouns, not verbs. URLs identify things, not actions.
Good:
GET /orders ā list orders
GET /orders/1234 ā get order 1234
POST /orders ā create an order
PUT /orders/1234 ā replace order 1234
PATCH /orders/1234 ā partially update order 1234
DELETE /orders/1234 ā delete order 1234
Bad:
GET /getOrder?id=1234
POST /createOrder
POST /updateOrder/1234
POST /deleteOrder/1234Nested Resources
Express relationships through URL hierarchy ā but only one level deep:
GET /orders/1234/items ā items belonging to order 1234
GET /orders/1234/items/5 ā item 5 of order 1234
POST /orders/1234/items ā add item to order 1234
Avoid deep nesting:
/customers/99/orders/1234/items/5/reviews ā too deep, use /items/5/reviewsRequest and Response Bodies
Use JSON as the default content type. Always include Content-Type: application/json on requests with a body.
Response envelope ā keep it flat:
// Good ā data at the root
{
"orderId": "ORD-1234",
"status": "confirmed",
"totalAmount": 299.99
}
// Avoid unnecessary wrapping
{
"data": {
"result": {
"order": { ... }
}
}
}Error response ā always structured:
{
"type": "https://errors.systemforge.io/validation-error",
"title": "Validation Failed",
"status": 422,
"detail": "The 'quantity' field must be a positive integer.",
"instance": "/orders/1234/items",
"errors": [
{ "field": "quantity", "code": "must_be_positive", "message": "Must be greater than 0" }
]
}This follows RFC 7807 Problem Details ā a standard error format that clients can parse consistently.
Idempotency in REST
Idempotency is what makes retries safe. Without it, a network timeout can cause duplicate operations.
Natural idempotency: GET, PUT, and DELETE are idempotent by definition.
Making POST idempotent: Use an idempotency key ā a client-generated unique ID included in the request header:
POST /orders HTTP/1.1
Content-Type: application/json
Idempotency-Key: client-uuid-7f3a9b2c
{
"customerId": "CUST-99",
"items": [...]
}The server stores the idempotency key and the result of the first successful request. If the same key arrives again (a retry), the server returns the cached result without re-executing the operation.
Storage: Idempotency keys should be stored with a TTL (typically 24 hours). After expiry, re-processing is acceptable since it is no longer a retry.
API Versioning
APIs change. Consumers break when they change unexpectedly. Versioning prevents this.
Versioning Strategies
URI path versioning (most common, most visible):
/api/v1/orders
/api/v2/ordersHeader versioning (clean URLs, less discoverable):
GET /api/orders HTTP/1.1
Accept: application/vnd.systemforge.v2+jsonQuery parameter versioning (avoid ā pollutes query strings, harder to cache):
/api/orders?version=2Semantic Versioning Rules
- Non-breaking changes (new optional fields, new endpoints): no version bump required
- Breaking changes (removed fields, changed field types, changed behaviour): increment major version
- Support the previous major version for a minimum agreed deprecation period (typically 6ā12 months)
What Constitutes a Breaking Change
Breaking:
- Removing a field from the response
- Changing a field's type (string ā integer)
- Renaming a field
- Changing an endpoint URL
- Making an optional field required
- Changing HTTP method for an operation
- Removing an enum value
Non-breaking:
- Adding a new optional field to the response
- Adding a new optional request parameter
- Adding a new endpoint
- Adding a new enum valueOpenAPI Contracts
Define your API as an OpenAPI (Swagger) specification before writing a line of implementation code. The spec is the contract between you and your consumers.
openapi: "3.1.0"
info:
title: Orders API
version: "2.0"
paths:
/orders:
post:
summary: Create an order
operationId: createOrder
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateOrderRequest"
responses:
"201":
description: Order created
headers:
Location:
schema:
type: string
description: URL of the created order
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
"422":
description: Validation error
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetails"Benefits of contract-first design:
- Consumers can generate client SDKs from the spec before the server is built
- Mock servers can be generated from the spec for parallel development
- The spec can be validated in CI to catch breaking changes before deployment
Resilience Patterns for REST Clients
Synchronous clients must be resilient. Networks are unreliable. Servers fail. Build resilience in:
Timeouts
Always set two timeouts:
- Connection timeout ā how long to wait for the TCP handshake to complete (typically 2ā5 seconds)
- Read timeout ā how long to wait for the server to return the full response (typically 10ā30 seconds, depending on the operation)
Never use infinite timeouts. They cause threads to block indefinitely and cascade into service-wide unavailability.
Retry with Exponential Backoff
Retry 5xx and 429 responses ā but not 4xx (those require request changes):
Attempt 1 ā 503 Service Unavailable
Wait: 1s + jitter
Attempt 2 ā 503 Service Unavailable
Wait: 2s + jitter
Attempt 3 ā 503 Service Unavailable
Wait: 4s + jitter
Attempt 4 ā 200 OK āJitter (a random offset) prevents all retrying clients from hitting the recovering server at the same instant (the thundering herd problem).
Only retry idempotent operations. Never retry a POST without an idempotency key ā you risk creating duplicates.
Circuit Breaker
A circuit breaker monitors call failure rates. When failures exceed a threshold, it opens ā subsequent calls fail immediately without hitting the server, giving it time to recover.
CLOSED ā calls pass through, failures counted
(failure rate > threshold)
ā
OPEN ā calls fail immediately, no server contact
(after timeout)
ā
HALF-OPEN ā one test call allowed through
(success) ā CLOSED
(failure) ā OPENLibraries: Polly (.NET), Resilience4j (Java), resilience (Python).
Bulkhead
Isolate resource pools so that failures in one integration do not starve resources for others. Use separate HTTP client instances (with separate connection pools and thread limits) for different downstream services.
Caching
HTTP caching reduces latency and load on servers. Use it aggressively for read-heavy APIs.
Cache-Control Headers
Cache-Control: public, max-age=300 ā cache for 5 minutes
Cache-Control: private, max-age=60 ā user-specific, 1 minute
Cache-Control: no-cache ā revalidate before using cached copy
Cache-Control: no-store ā never cache (sensitive data)ETags (Conditional Requests)
// First request
GET /orders/1234
Response: ETag: "abc123"
// Subsequent request
GET /orders/1234
If-None-Match: "abc123"
Response: 304 Not Modified ā no body transferredETags allow clients to check whether a resource has changed without downloading the full response. Essential for polling patterns.
Pagination
Never return unbounded collections. Implement pagination from day one:
Cursor-based pagination (preferred for real-time data):
{
"items": [...],
"nextCursor": "eyJpZCI6MTAwfQ==",
"hasMore": true
}Offset-based pagination (simpler, works for stable datasets):
{
"items": [...],
"page": 2,
"pageSize": 20,
"totalCount": 487
}Cursor-based is preferred for feeds and event streams because it handles concurrent inserts correctly ā a new record inserted at the top does not shift items across pages.
REST vs. Other Synchronous Options
| Option | When to prefer it | |--------|-------------------| | REST/HTTP | External APIs, public interfaces, browser clients, wide ecosystem compatibility | | gRPC | Internal service-to-service calls where performance matters; strongly typed contracts; streaming | | GraphQL | Multiple clients with different data shape needs; reduces over-fetching | | SOAP | Legacy systems, financial/healthcare environments requiring WS-Security or ACID transactions |
Lesson Summary
- Request/Response is the right pattern when the caller genuinely cannot continue without the result and the operation completes quickly.
- HTTP verb semantics (safe, idempotent) determine how retries, caches, and proxies behave ā use them correctly.
- Status codes communicate who is responsible for a failure:
4xx= caller must fix the request;5xx= server failure, may retry. - Idempotency keys make POST safe to retry ā store the key and cached result server-side with a TTL.
- Always version APIs, define breaking change policy, and use OpenAPI specs as the authoritative contract.
- Resilience is the caller's responsibility: set timeouts, retry with exponential backoff and jitter, implement circuit breakers.
Next: Event-Driven Architecture
API Design Principles Knowledge Check
5 questions Ā· Test what you just learned Ā· Instant explanations
Enjoyed this article?
Explore the Integration Engineering learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.