Lesson 39 of 159
Written lesson · 16 min

HTTP Through a Backend Engineer's Eyes

HTTP as a contract: methods, safety, idempotency, retries and server authority.

Preview

You are viewing a free preview lesson.

So far, we have designed the internal structure of our Order Management Backend: Handlers call use cases, use cases work with the domain, and repositories persist it. We have modeled Product, Inventory, Order, OrderItem, CustomerId and OrderStatus. Now we move outward and ask how an external client communicates with the application. For our architecture, the answer is HTTP, JSON and a REST-style API.

Understanding HTTP as a backend engineer is not about memorizing GET, POST, PUT, PATCH and DELETE. HTTP is part of the application's observable behaviour, and a useful mental model is:

HTTP is a behavioural contract between the client and the server.

A request says which resource the client addresses, which operation it requests and which values it controls; the contract also says what a repeated request does and how success and expected failure look. An API can work while still being confusing, unsafe or hard to integrate with, so good backend engineering treats HTTP semantics as part of the design.

HTTP Is an Application Boundary

Our domain does not know about HTTP. A call to order.cancel() does not know whether it came from POST /api/v1/orders/{orderId}/cancel, a test or some future internal entry point.

The HTTP boundary receives the request, validates and maps its representation, obtains the authenticated context, invokes the use case, and translates the result or failure into a response:

Drawing diagram…

HTTP stays at the edge: it is our external contract, not a replacement for the architecture behind it.

Anatomy of an HTTP Request

Consider an Order-creation request:

http
POST /api/v1/orders
Content-Type: application/json
Authorization: Bearer ...

{
  "items": [
    {
      "productId": "59ba787c-6d83-4a21-9bae-19c3d0469bd5",
      "quantity": 2
    }
  ]
}

Several parts are involved, and each has a different purpose:

  • The method (POST) communicates the kind of interaction being requested.
  • The URI (/api/v1/orders) identifies the resource or capability being addressed.
  • The headers (Content-Type, Authorization, Accept, …) carry request metadata.
  • The body carries the Order-creation input the client controls.

HTTP Methods Communicate Intent

Methods are not interchangeable labels. Technically, server code could perform the same Java operation from any method, but protocol semantics matter. GET /api/v1/orders/{orderId} and POST /api/v1/orders/{orderId}/cancel communicate fundamentally different intent: the first reads state, the second asks the system to change business state.

GET Is for Reading

GET /api/v1/products means "give me a representation of the Products I am allowed to browse", and GET /api/v1/orders/{orderId} means "give me this Customer-owned Order". A GET never requests a business mutation such as decreasing Inventory, cancelling an Order or changing a price.

Imagine that GET /api/v1/orders/{orderId}/cancel actually cancelled the Order. Browsers, caches, proxies, monitoring systems, link previewers and crawlers treat GET as a safe read and may issue, repeat or cache GET requests on that assumption.

Never hide a meaningful business mutation behind GET.

Normal GET input travels in path and query parameters. Even if a stack accepts a body on GET, the body has no generally defined meaning in HTTP, so do not build query APIs around it.

Safe Methods

A method is safe when the client is not requesting a business-state change. GET is the safe method that matters most to us (HEAD and OPTIONS are safe too). Safe does not mean nothing changes anywhere: access logs, metrics and cache activity are operational side effects, while the requested business operation stays read-only.

Idempotency

Another crucial HTTP concept asks:

If the same operation is repeated, does the intended final effect remain the same as performing it once?

That property is idempotency. "Set Inventory available quantity to 20" leaves 20 whether it runs once or several times, so it is naturally idempotent. "Create another Order" performed twice may produce Order A and Order B, so Order creation is not naturally idempotent.

Idempotency is about effect, not about identical responses. Suppose an idempotent resource-removal operation succeeds once. A repeated request may return a different response because the resource is already gone, yet after the first successful application the target state is reached and it stays reached after every repetition. Think about the resulting system effect, not byte-for-byte response equality.

A Timeout Does Not Mean the Operation Failed

Networks fail ambiguously. If the client sends POST /api/v1/orders and sees a timeout, it knows only that it did not receive a response: the request may never have arrived, or the Order may have been committed and only the response lost. That is fundamentally different from a 400 the client actually receives. A blind retry may create a second Order, so retry behaviour must be designed from the operation's semantics (Modules 9 and 13 return to this).

POST, PUT, PATCH and DELETE

POST

POST commonly creates a new subordinate resource, triggers a command-like operation, or performs an operation that is not naturally idempotent. Our canonical Order-creation endpoint is POST /api/v1/orders. The client expresses "create an Order containing these requested Products and quantities", and the server creates a new Order with server-owned identity, ownership, prices, creation time and lifecycle state.

POST is not "anything that changes data". A weak API might contain POST /getProducts, POST /updateProduct, POST /deleteProduct and POST /cancelOrder: everything becomes POST because the implementation is easy. Much of HTTP's semantic value is then lost, because a client can no longer infer anything from the method and URI together. Our API stays resource-oriented and operation-aware.

PUT

PUT generally sets or replaces the state of a resource at a known URI, and the property that matters to us is that repeating the same replacement leads to the same intended state. Our Inventory API is a good example:

http
PUT /api/v1/inventory/{productId}
Content-Type: application/json

{
  "availableQuantity": 20
}

This means "set the current available quantity for this Product to exactly 20". Calling it once or repeatedly produces the same final quantity, which matches the semantics of our SetInventoryUseCase.

PUT is not "increment". If the body were {"increaseBy": 20}, repeated requests would move the quantity from 10 to 30 to 50, which is not idempotent. The method name alone does not create idempotency; the implementation and the business operation must honour the contract.

PATCH

PATCH represents partial modification of an existing resource. Our canonical Product update is PATCH /api/v1/products/{productId} with a body such as {"priceCents": 8990}, meaning "change this Product's current price to 8990 cents and leave omitted fields unchanged" (the next lessons settle the exact field semantics).

PATCH is not automatically idempotent. Repeating our "set priceCents to 8990" still leaves 8990, so it is idempotent in effect, but a body like {"increasePriceCentsBy": 1000} would move the price from 8990 to 9990 to 10990 and would not be.

The business meaning determines the effect.

The HTTP method communicates expectations, but it cannot fix incorrect semantics.

DELETE and Domain Meaning

DELETE usually communicates "remove this resource". Our Product domain does not support physical deletion; its business operation is "deactivate Product". Blindly exposing DELETE /api/v1/products/{productId} would communicate stronger semantics than the business provides, so our canonical API uses POST /api/v1/products/{productId}/deactivate and makes the business operation explicit. The HTTP contract should preserve server-controlled domain meaning; the next lesson designs these endpoints.

Resource-Oriented URIs

Our primary resources are products, inventory and orders:

http
GET  /api/v1/products
GET  /api/v1/products/{productId}
POST /api/v1/orders
GET  /api/v1/orders
GET  /api/v1/orders/{orderId}

These beat RPC-like paths such as /getAllProducts or /findOrderById, because the HTTP method already carries part of the operation's meaning. The URI identifies the resource, not our Java architecture: /orderHandler/create and /createOrderUseCase leak internals, and clients should not know whether we use handlers, use cases, JPA or PostgreSQL. Persistence does not shape the URI either: OrderRepository.save() does not justify POST /saveOrder. Endpoint design starts from resources, business capabilities and use cases, not from repository methods, class names or tables.

Collections use plural names (/products, /orders), and Inventory is its own collection (/inventory). The point is a coherent contract: if GET /api/v1/products/{productId} reads one Product, GET /api/v1/orders/{orderId} follows the same pattern, so clients predict behaviour with less documentation. Consistency beats cleverness. An API where cancellation is /orders/{id}/cancel, deactivation is /products?action=disable and Inventory update is /updateInventory makes every endpoint a surprise.

Path and Query Parameters

In GET /api/v1/orders/{orderId}, the path identifies which Order. At the HTTP boundary the value is a string such as d2e7b44e-463f-4bbc-b16f-b1c80bc80442; the handler parses it into a UUID and then an OrderId, so the domain receives a typed identity rather than HTTP path text.

Query parameters commonly adjust collection reads, as in GET /api/v1/products?page=0&size=20 (zero-based page, default size 20; 5.08 defines the full pagination contract). Query parameters suit pagination, filtering and sorting where those features exist. They must not secretly trigger state-changing commands. GET /api/v1/orders/{orderId}?cancel=true looks like a read while a query parameter changes business state; our cancellation is explicit, as POST /api/v1/orders/{orderId}/cancel.

Headers and the Authorization Header

Headers carry metadata: Content-Type, Authorization, Accept, request or correlation identifiers. Content-Type: application/json states the representation format; our v1 API uses only application/json, so we need no complicated content negotiation.

Credentials commonly arrive as Authorization: Bearer ..., and neither handlers nor the domain parse this header. Spring Security reads it and establishes an authenticated identity, which reaches handlers and use cases as a CustomerId and authorities: meaningful identity, not raw protocol details.

Status Codes and Errors Are Part of the Contract

A response is more than JSON: 201 Created, 404 Not Found or 409 Conflict states the broad outcome before the client reads the body, and a failure reported as 200 OK with {"success": false} forces every client to ignore HTTP semantics and parse a custom field. Two decisions already shape our contract. An expected business rejection such as cancelling a PAID Order is a 409, not a 500, even though Java represents it with an exception; and a Customer asking for another Customer's Order gets the same 404 as for a missing one, so we never leak whether it exists. Lesson 5.05 maps every outcome to a status.

The body of a failure follows one structured contract, RFC 9457 Problem Details, with a stable application code such as ORDER_NOT_FOUND and field-level errors for validation. Clients never receive SQL, stack traces or Java class names. Lesson 5.07 designs and implements that contract.

The HTTP Contract Is Not the Domain Model

Our domain uses OrderStatus.UNPAID, and the API may expose {"status": "UNPAID"}. That representation belongs to the DTO and the API contract. The domain does not need Jackson annotations or HTTP concerns just to make the JSON work, and request and response DTOs stay separate from domain behaviour.

Server Authority and Client Intent

Consider Create Order. The client controls only the ProductIds and quantities; the server determines everything else:

ValueSource
CustomerIdThe authenticated caller
OrderIdA generated UUID
unitPriceCentsThe current authoritative Product price
statusUNPAID
createdAtThe application Clock
totalCentsDerived from the OrderItems

That distinction must be visible in the HTTP contract. This request design is wrong:

json
{
  "customerId": "another-customer",
  "status": "PAID",
  "totalCents": 100,
  "items": [
    {
      "productId": "...",
      "quantity": 2,
      "unitPriceCents": 1
    }
  ]
}

The client must not control those values. A better API lets clients express intent ("create this Order", "cancel my Order", "pay my Order"), and the server decides who owns the Order, what it costs, whether a transition is legal, whether Inventory can be consumed, whether Payment has succeeded and which state is persisted. This aligns HTTP with our domain architecture.

Explicit Business Commands

Creating, reading and updating a Product map naturally to resource operations. Deactivating a Product, cancelling an Order and paying an Order are state transitions, and exposing them as generic field mutation makes the API less accurate. PATCH /api/v1/orders/{orderId} with {"status": "CANCELLED"} suggests that the client can assign Order status directly, while POST /api/v1/orders/{orderId}/cancel means "attempt to cancel this Customer-owned Order" and preserves the business rules inside order.cancel(). Likewise, POST /api/v1/orders/{orderId}/pay means "attempt to pay this Order", not "set the status to PAID": only a confirmed Payment may lead to order.markPaid(). HTTP should communicate business intent rather than expose arbitrary domain mutation.

Retries and Statelessness

Without a deliberate idempotency mechanism, POST /api/v1/orders sent twice may create two separate Orders, even if the bodies are identical, and a repeated pay could call the Payment provider twice: order.markPaid() rejecting PAID → PAID protects one Order's validity, not against duplicate execution. The API must not pretend otherwise; Module 9 designs Payment retry safety and Module 13 adds an explicit request idempotency contract. Idempotency is a system property: "PUT is idempotent" does not make a handler that increments on every PUT correct, and "POST is non-idempotent" does not stop us from making an operation idempotent with an idempotency key.

A REST-style request also carries or references everything the server needs. If request 1 creates an Order and request 2 pays it, request 2 names the OrderId and carries the caller's authentication; it never relies on the JVM remembering an Order from request 1. Durable state belongs in PostgreSQL, and per-request state never lives in shared Spring beans (5.03). An HTTP request is also not a database transaction: transaction boundaries belong to the application workflow.

The Observable Contract

Internally, one request may involve several classes, repository operations, a transaction and domain construction. The client observes only the method, URI, headers, request body, status code and response body: that is the public contract. If we later optimize a repository, a query or the persistence mapping without changing behaviour, clients face no API migration.

API design is product design. For cancellation we decide which Orders can be cancelled, what counts as success, and what happens if the Order is already PAID or belongs to another Customer. HTTP semantics also do not replace business documentation: POST /api/v1/orders does not say that duplicate ProductIds are rejected, that the server price is authoritative or that all items succeed atomically. OpenAPI and product documentation describe those rules.

A Useful Endpoint Design Sequence

When designing an endpoint, do not begin with "which Spring annotation should I use?". Ask instead:

  1. What business operation or resource is involved?
  2. Is it a read or a mutation?
  3. What does the client control?
  4. What remains server-controlled?
  5. Is the operation naturally idempotent?
  6. Which URI best represents the resource or operation?
  7. Which successful outcome should the client observe?
  8. Which expected failures exist?
  9. Which HTTP semantics represent those outcomes?

Only then do we implement the handler and choose syntax such as @GetMapping(...) or @PostMapping(...). The annotation does not design the API; it implements the design.

Practice Exercise 1: Safe, Idempotent or Neither?

Solutions: lesson 5.13.

For each request, say whether it is safe, idempotent in effect, or neither, and whether a client may simply resend it after a timeout:

  1. GET /api/v1/orders/{orderId}
  2. PUT /api/v1/inventory/{productId} with {"availableQuantity": 20}
  3. POST /api/v1/orders with two items
  4. PATCH /api/v1/products/{productId} with {"priceCents": 8990}
  5. A proposed PATCH /api/v1/products/{productId} with {"increasePriceCentsBy": 1000}
  6. POST /api/v1/orders/{orderId}/cancel, sent again after the first request succeeded

Practice Exercise 2: Trace Each Value to Its Source

In your project, open Order.java, OrderItem.java and CreateOrderCommand.java from module 4. For every value that Order.create(...) and the OrderItem constructor need, say where it comes from when a Customer sends POST /api/v1/orders: the JSON body, the authenticated caller, or the server (generated, looked up or read from the clock). Then explain why CreateOrderCommand.Item carries no price.

Knowledge Check

Question 1

Nobody clicks twice, yet a GET that cancels an Order is still dangerous. Why?

Question 2

A request that removes a resource answers 204 the first time and 404 the second. Is the operation idempotent?

Question 3

A client sends POST /api/v1/orders and gets a timeout. Why can it not conclude that no Order exists, and what does a blind retry risk?

Question 4

Why does deactivation use POST /api/v1/products/{productId}/deactivate instead of DELETE /api/v1/products/{productId}?

Question 5

A Customer's app knows its own Customer ID. Why does the Create Order body still not carry it?

Knowledge Check Answers

Answer 1

Browsers, proxies, caches, link previewers and crawlers treat GET as safe and may issue or repeat it on their own. A prefetch or a retry would cancel the Order without any user intent.

Answer 2

Yes. Idempotency is about the effect: after the first success the resource is gone, and every repetition leaves it gone. The responses may differ.

Answer 3

A timeout only says that no response arrived: the request may have been lost, or the Order may have been committed and only the response lost. Because Order creation is not idempotent, a blind retry can create a second Order.

Answer 4

The business operation is deactivation, not removal: the Product keeps existing because Orders reference it. DELETE would promise stronger semantics than the domain provides.

Answer 5

Ownership is server-controlled. If the body carried a customerId, a client could send someone else's, so the server takes the identity from authentication and accepts only intent (Products and quantities) from the body.

Engineering Principle

HTTP is not merely a transport pipe; its methods, resource semantics, status codes and retry behaviour are part of the application's public behavioural contract.

Clients express intent while the backend stays authoritative over identity, prices, totals, ownership and state transitions. We design the operation first, choose the HTTP representation second and write the Spring annotation last.

Summary

  • HTTP is the application's external contract: the handler owns the boundary and the domain stays transport-independent.
  • GET is a safe read that never mutates; idempotency is about the final effect, not identical responses.
  • A timeout leaves the outcome ambiguous, so a blind retry of POST /api/v1/orders can create a second Order.
  • POST creates, PUT sets exact state, PATCH partially updates, and deactivation, cancellation and payment are explicit POST commands rather than DELETE or status assignment.
  • URIs use resource vocabulary, never Java class or repository method names; query parameters page and filter but never mutate.
  • Status codes and the Problem Details error body are part of the contract (5.05, 5.07).
  • Clients never control ownership, prices, totals, IDs, status or creation time; requests carry no hidden server-side conversation state.
  • The contract survives internal refactoring, still needs business documentation, and comes before any Spring annotation.

In the next lesson, Designing Resources and Endpoints, we design the concrete API surface for Products, Inventory and Orders, including the decision to expose no Customer resource solely for ordering.

Think first

Coming soon

Checkpoint questions that ask you to commit to an answer before the lesson shows it.