Tuesday, October 13, 2026

The 100 Percent Busy Falacy

Keeping every engineer busy feels efficient. Queueing theory says it is the fastest way to slow everything down.

In many engineering organizations, slack time is treated as a sin. If a backend developer finishes early, they get another ticket. If a QA engineer is waiting for a build, they get a regression script to run. Every dashboard is green, every sprint is full, and everyone is working hard.

And yet features take months to reach customers.

This is not a people problem. It is a math problem, and once you see the math, you cannot unsee it.

Software Is Not a Factory Floor

Maximizing resource utilization, the share of time each person is busy, makes intuitive sense to anyone who has managed a production line. When the work is predictable and the parts are identical, keeping every machine running is a sound strategy.

Software development is not like that. We rarely build the same feature twice. Work arrives unpredictably, tasks take longer than expected, and bugs appear without warning. It is creative, highly variable work, and optimizing it for utilization destroys something more important: flow efficiency, the share of time a feature is actually being worked on rather than waiting in line.

The Highway Analogy

Think of a highway.

  • At 50% utilization, cars move at the speed limit. If one driver taps the brakes, the system absorbs it.
  • At 90% utilization, traffic crawls. A single lane merge sends a wave of brake lights back for miles.
  • At 100% utilization, every square foot of asphalt is covered by a car. The highway is perfectly “utilized,” and the speed is zero. You have built a parking lot.

A siloed engineering organization runs like that full highway. Because everyone is fully booked, there is no spare capacity. When a frontend developer needs a small backend change, the backend team is already committed to its own sprint. The request goes into a backlog, and it waits.

The Math Behind the Traffic Jam

The highway is not just a metaphor. Queueing theory describes it precisely. Kingman’s formula approximates the average waiting time in a queue:

Wait ≈ ( ρ / (1 − ρ) ) × ( (Ca² + Cs²) / 2 ) × Service time

Here ρ is utilization, and Ca and Cs measure how unpredictable the arrivals and the work are.

Look at the first term, ρ / (1 − ρ). At 50% utilization it equals 1. At 80% it equals 4. At 90% it equals 9. At 95% it equals 19. The curve stays fairly flat until around 70–80%, then bends sharply upward, and as utilization approaches 100%, the wait approaches infinity.

The second term makes it worse for software. Our variability is high, so every point of utilization costs us more waiting than it would cost a predictable production line.

Now chain it together. A feature that has to pass through four horizontal teams, each running near full utilization, joins four queues in a row. The time spent coding barely matters; the time spent waiting dominates.

The Customer’s Clock vs. the Developer’s Clock

This is why two metrics tell such different stories:

  • Cycle Time is the developer’s clock: from starting active work on a task to finishing it.
  • Lead Time is the customer’s clock: from the moment an idea or request exists to the moment it is live in production.

Siloed organizations often report excellent cycle times. A backend developer picks up a ticket, writes the API, and closes it in two days. But that API then waits for the frontend team’s next sprint, then for the DBA queue, then for QA. The lead time for the feature might be sixty days.

When managers tell executives that velocity is high because cycle times are short, while customers wait months, trust erodes. Customers do not care how fast we type. They care how long they wait.

Fewer Handoffs, Fewer Queues

You cannot make variability disappear; it is the nature of creative work. But you can remove the queues. Every handoff between teams is a queue, so the most powerful lever is reducing the number of handoffs a feature needs.

That is what cross-functional feature teams do. When the people who can build the UI, the API, the database change, and the tests sit on the same team, a feature no longer joins four queues. A question that used to take three days to route through a ticket gets answered across the table in five minutes. You move back down to the flat, fast end of Kingman’s curve.

It also means accepting something that feels uncomfortable: a team with some slack is not wasting money. That slack is what lets the team absorb the unexpected without stopping the line.

Wrapping Up

The 100% busy organization is optimizing the wrong number. Utilization measures how busy people are; lead time measures how fast value reaches customers. Push utilization toward 100% and queueing math guarantees that lead time will grow sharply. Measure the customer’s clock instead, remove the handoffs that create queues, and leave room in the system to breathe. Busy is not the same as delivering.


This article is adapted from Chapter 2 of my upcoming book, Vertical Velocity: How to Break Silos, Ship Faster, and Reclaim Engineering Joy. The chapter goes further: how to put a price on your queues with Cost of Delay, and how to make the financial case for restructuring to a skeptical CFO. Follow this blog to hear when it’s out.

Monday, October 12, 2026

The Anatomy of a Feature Team

A database schema is not a feature. Here is what a feature really is, and what the team that builds one looks like.

If you want to build an effective feature team, you first have to agree on what a “feature” is. In most traditional organizations, that definition is broken.

The “Feature” Hijack

In organizations built around component teams, the word “feature” gets hijacked by technical jargon. You will hear teams say, with some pride:

  • “We are building the user registration database schema feature.”
  • “We are building the OAuth integration backend API feature.”
  • “We are building the responsive login page UI mockup feature.”

None of these are features. They are components: horizontal building blocks. On its own, a database schema, an API endpoint, or a frontend mockup is worth exactly nothing to the people using your product. A customer cannot log in with a schema, and cannot buy anything with a mockup.

Redefining the Feature: The Vertical Slice

A feature is a complete, user-facing capability that delivers a specific, measurable outcome. It is defined by the customer’s experience, not by our technology stack.

Consider this user story: “As a customer, I want to pay for my subscription using Apple Pay so that I can check out securely in under ten seconds.”

That is a feature. To deliver it, changes must happen in the database, the backend processing engine, the payment gateway integration, and the mobile user interface. It cuts vertically through every layer of the stack, which is why we call it a vertical slice. A large feature can be split into thinner slices, but each slice must still cut through every layer and work end to end.

This changes how we plan, estimate, and deliver. Instead of tracking five technical dependencies across five teams, we track one customer capability moving through one cross-functional team. We break work down by the value it delivers, not by the systems it touches.

The Anatomy of the Pod: The Cross-Functional Cell

If a feature is a vertical slice of the technology stack, then the team that builds it must be a vertical slice of the organization. We call this a Feature Pod.

A Feature Pod is a small, autonomous team designed to minimize dependencies on other teams. It is not a department and not a temporary project team. It is a stable, long-lived team aligned with a specific domain or value stream, so it keeps the knowledge it builds.

A typical pod has 3 to 7 core members. The roles below are skills the pod needs, not one person each. T-shaped engineers, deep in one specialty and able to help in neighboring ones, let a small pod cover them all, and DevOps or UX may be shared with a neighboring pod.

1. The Product Owner: the anchor. The Product Owner (PO) works with the engineers every day, not from a distant product department. They hold the vision for the backlog, prioritize work based on customer feedback, and clarify business rules in real time. They own the what.

2. The cross-functional engineering core.

  • Frontend developers build the user interface, manage client-side state, and polish the interactions.
  • Backend developers own API design, databases, system integration, and business logic.
  • QA and test engineers are embedded champions of quality. They don’t wait for finished code to write manual test scripts; they agree on acceptance criteria with the team before coding begins, and build automated regression suites.
  • DevOps and infrastructure give the team the tooling, pipelines, and cloud resources to deploy its own work.
  • UX designers collaborate instead of handing off. Design is woven into the code, not left in the mockups.

3. The Scrum Master or agile coach (optional or shared). They facilitate the sprint process, help the team surface blockers, and coach it toward self-organization. They own the process.

How the Scrum Cadence Changes

With component teams, Scrum is fragmented. The backend team has its standup, the frontend team has its own, and “integration” becomes a separate, dreaded project phase.

In a Feature Pod, Scrum becomes one conversation:

  • Sprint Planning. The team commits to completed features, or vertical slices of them, not technical tasks. It doesn’t commit to “three backend tickets and two frontend mockups.” It commits to “user registration works end to end.”
  • Daily Scrum. Instead of “Yesterday I worked on API endpoint X,” you hear “Yesterday I paired with Sarah to connect the registration form to the backend validator. We hit a validation bug, and we’ll fix it today.” The focus is on getting the user story across the finish line.
  • Sprint Review. The team demonstrates working software. No slides of database schemas or API definitions: the team runs the real customer flow, end to end, in a staging environment.

Wrapping Up

Feature teams start with a definition. As long as “feature” means a schema, an endpoint, or a mockup, work will be split by layer, and every customer outcome will wait on a chain of handoffs between teams. Define the feature by the customer’s experience, build a team that holds every skill needed to deliver it, and the rest follows: planning, standups, and reviews all start to revolve around working software instead of finished components.

That is the core of Vertical Velocity: teams that deliver complete business value end to end, without waiting on other teams.


This article is adapted from Chapter 3 of my upcoming book, Vertical Velocity: How to Break Silos, Ship Faster, and Reclaim Engineering Joy. The book goes further: how to slice a large feature into vertical slices, how to move from component teams to feature pods without stalling delivery, and how to measure whether it’s working. Follow this blog to hear when it’s out.

Thursday, September 3, 2026

Achieving Vertical Velocity Through API-First Design

In today's fast-paced digital landscape, organizations are constantly seeking ways to deliver value more rapidly and efficiently. I have coined the concept of "Vertical Velocity"— the ability of cross-functional teams to deliver complete end-to-end business value continuously without waiting on or blocking other teams. But achieving this velocity requires more than just reorganizing teams; it demands a fundamental shift in how we think about system boundaries and interfaces.

The Three Levels of Alignment

True Vertical Velocity doesn't happen by accident. It requires seamless alignment across three critical levels:

Organizational: Stream-aligned feature pods with end-to-end domain ownership that can operate independently and make autonomous decisions.

Architectural: Well-defined bounded contexts designed with clear business domain boundaries that reflect how your business actually operates.

Technical: Intentionally designed API interfaces—whether governed by explicit schemas or self-describing hypermedia controls—that establish clear, resilient boundaries between services.

When these three levels are in harmony, teams can move independently, deploy autonomously, and deliver value continuously. But when they're misaligned, even the most empowered agile feature pods will stall against rigid, brittle software boundaries.

Understanding Conway's Law in Practice

Any serious discussion about aligning team structure with technical boundaries inevitably leads to Conway's Law: Organizations which design systems are constrained to produce designs which are copies of the communication structures of these organizations.

If you want cross-functional feature teams to thrive, you must design boundaries that allow them to operate independently. In Domain-Driven Design (DDD), these are called Bounded Contexts. By drawing hard boundaries around specific business domains, you allow teams to own a domain entirely.

But how do those bounded contexts communicate? Through APIs.

Rethinking Our Relationship with APIs

Historically, we treated APIs simply as pipes connecting one server to another—passive, data-centric, and brittle. Every schema change breaks consumers, and business logic leaks into the client layer. These "Pipe APIs" were afterthoughts, accidental byproducts of implementation rather than deliberate design decisions.

In a modern ecosystem, however, an API is the primary user interface of your bounded context. We need to build "Product APIs" that expose business capabilities, not database tables. A Product API encapsulates business rules, ensures data validity before persistence, and provides semantic, meaningful operations to its consumers.

When you treat your API as an afterthought—as a simple data pipe—you invite cognitive load, integration nightmares, and API drift: the gradual divergence between your documented API contracts and your actual implementation, often caused by developers forgetting to update wiki pages or README files when they make changes.

The Principle of API-First Design

The principle is straightforward: treat your API—its semantics, behavior, and constraints—as a first-class concern in your architecture, not an accidental byproduct of your implementation. This is API-First Design.

But "API-First" doesn't mean "OpenAPI-First" or "Contract-First." It means something more fundamental: that the interface between services deserves deliberate design, regardless of the paradigm you choose.

Choosing the Right Approach

There are multiple valid approaches to API design, each with its own strengths:

Contract-First: You publish a machine-readable specification that describes every endpoint, request, and response. Clients consume this contract and hardcode their interactions accordingly. This offers exceptional tooling, type safety, and validation—but at the cost of tighter coupling. Any change to the contract requires coordination.

Hypermedia-Driven: You design your API to guide clients through hypermedia links embedded in responses. Clients discover available actions dynamically, navigating from resource to resource without hardcoding URL structures. This offers loose coupling and evolutionary flexibility—but requires more sophisticated clients and a deeper understanding of REST principles.

Both approaches are "API-First." Both require deliberate design, clear semantics, and a focus on the consumer experience. The difference lies in how you communicate your API's capabilities.

The Right Tool for the Right Context

Different contexts call for different approaches:

  • Where data integrity is paramount, Contract-First with OpenAPI shines
  • Where discoverability is key, Hypermedia-Driven REST with HATEOAS excels
  • Where temporal decoupling matters, Event-Driven architectures make sense
  • For high-performance internal communication, protocols like gRPC deliver

What unifies these approaches is a shared commitment to designing APIs intentionally, with the consumer in mind, and treating the interface as a boundary that enables parallel work and independent evolution.

Unlocking the Benefits

When your API is designed intentionally—regardless of the paradigm—you unlock transformative benefits:

  • UI developers can build and test against clear expectations before the backend exists
  • Backend developers have clear guidance on what to implement
  • Integration teams can validate compatibility automatically
  • Evolution becomes manageable through versioning, deprecation policies, or the inherent flexibility of hypermedia

The Lost Promise of REST

Most of what we call REST today is actually RPC-over-HTTP—a collection of remote procedure calls that happen to use HTTP as a transport. True REST, with hypermedia as the engine of application state, offers powerful decoupling but has been largely ignored in practice. There are good reasons for this: hypermedia-driven REST requires sophisticated clients, adds complexity, and demands a deeper understanding of architectural principles. For many use cases—especially external APIs where clients need predictability—a contract-first approach is simply more practical.

The lost promise of REST isn't a failure of REST itself. It's a failure to match the right approach to the right context. Sometimes you need a map (explicit contract). Sometimes you need signposts (hypermedia). And sometimes you need something else entirely.

The Path Forward

Achieving true Vertical Velocity requires a commitment to intentional API design. It means designing boundaries that allow teams to operate independently, choosing the right interface patterns for each context, and treating your API as the critical business asset it truly is.

The principle is constant: design your API first. The specific tool depends on your context. But the commitment to deliberate, consumer-focused design is what enables the organizational velocity that modern businesses demand.

Tuesday, August 4, 2026

The Pragmatic Guide to HTTP Status Codes and Error Design in Enterprise APIs

Navigating the complex world of HTTP status codes doesn't have to be a source of endless debate


As engineering teams build and scale enterprise APIs, few topics generate as much passionate discussion as HTTP status code selection. While status codes like 200 OK, 404 Not Found, and 500 Internal Server Error seem straightforward, the real challenges emerge when we venture into the nuanced territory of client errors—those dreaded 4xx responses.

Should you return a 400 Bad Request or a 422 Unprocessable Entity when a customer's order exceeds their credit limit? Is 409 Conflict appropriate for a subscription cancellation that's already been processed? What about idempotent operations that succeed but return no content—200 or 204?

These questions plague API designers and backend engineers alike. This guide offers a pragmatic, production-ready approach to HTTP status code selection, complete with Spring Boot 3 implementations that follow RFC 9457 standards.


The Decision Tree: Your HTTP Status Code Compass

When designing endpoint responses for error scenarios, validation failures, or state mutations across microservices, follow this decision workflow to consistently select the appropriate status code:

  1. Is the request syntactically malformed?

    • Yes → 400 Bad Request
    • No → Continue
  2. Is the client properly authenticated?

    • No → 401 Unauthorized
    • Yes → Continue
  3. Does the client have permission to perform this operation?

    • No → 403 Forbidden
    • Yes → Continue
  4. Does the target resource exist?

    • No → 404 Not Found
    • Yes → Continue
  5. Does the request conflict with the resource's current state?

    • Yes → 409 Conflict
    • No → Continue
  6. Is the request's precondition (e.g., ETag) met?

    • No → 412 Precondition Failed
    • Yes → Continue
  7. Does the request violate domain business rules despite valid syntax?

    • Yes → 422 Unprocessable Entity
    • No → Proceed with the operation

This decision tree provides clarity for even the most ambiguous error scenarios, ensuring your API responds predictably and consistently.


The Decision Matrix: 4xx Status Codes Demystified

Status CodeRFC NameSemantic MeaningEnterprise ScenariosAnti-Pattern to Avoid
400Bad RequestSyntactic or structural error in the request payloadMalformed JSON syntax, unparseable primitive types, missing mandatory headersReturning 400 for valid JSON payloads that fail domain business rules
401UnauthorizedAuthentication credential is missing or invalidMissing JWT bearer token, expired OAuth2 session, invalid API keyConfusing with 403 when identity is known but access rights are missing
403ForbiddenIdentity is known, but lacks permission for resourceUser with ROLE_VIEWER attempting a DELETE or PUT operationReturning 404 to hide resource existence unless explicitly required by security policy
404Not FoundTarget URI or entity primary key does not existQuerying /api/v1/orders/a0eebc99 when the record ID is absent in the databaseReturning 404 when a sub-resource query fails due to business permissions
409ConflictRequest conflicts with current state of resourceAttempting to cancel an order already marked CANCELLED or SHIPPEDReturning 400 when a request's syntax is valid but server state prevents execution
412Precondition FailedOptimistic locking header (If-Match) failedUpdating an entity whose ETag value has changed since last fetchAllowing silent overwrites (Lost Update Problem) on concurrent PUT operations
422Unprocessable EntityWell-formed syntax, but violates domain validationPlacing an order when account balance is insufficient or inventory is out of stockReturning generic 500 errors for expected, handling-worthy business rule failures
429Too Many RequestsRate limit or concurrency quota exceededExceeding Token Bucket capacity configured at Gateway or service mesh boundaryDropping requests silently without returning Retry-After headers

Spring Boot 3 Implementation: Embracing RFC 9457

Spring Boot 3 provides first-class support for RFC 9457 (Problem Details for HTTP APIs) through the org.springframework.http.ProblemDetail abstraction. This standardizes error responses across your API, making them machine-readable and developer-friendly.

Step 1: Define Custom Domain Exceptions

Start by creating domain-specific exceptions that map to HTTP status codes:

package com.coffeestream.shared.exception;

/**
 * Thrown when an operation cannot be completed due to the target resource's current state.
 * Maps to HTTP 409 Conflict.
 */
public class ResourceStateConflictException extends RuntimeException {
    private final String resourceId;
    private final String currentState;

    public ResourceStateConflictException(String resourceId, String currentState, String message) {
        super(message);
        this.resourceId = resourceId;
        this.currentState = currentState;
    }

    public String getResourceId() { return resourceId; }
    public String getCurrentState() { return currentState; }
}
package com.coffeestream.shared.exception;

/**
 * Thrown when a request violates domain rules despite valid syntax.
 * Maps to HTTP 422 Unprocessable Entity.
 */
public class BusinessRuleViolationException extends RuntimeException {
    private final String errorCode;

    public BusinessRuleViolationException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    public String getErrorCode() { return errorCode; }
}

Step 2: Implement Global Exception Handling

Use @RestControllerAdvice to centralize exception handling and convert your domain exceptions to RFC 7807 ProblemDetail responses:

package com.coffeestream.shared.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import java.net.URI;
import java.time.Instant;

@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    private static final String BASE_ERROR_URI = "https://api.coffeestream.com/errors/";

    @ExceptionHandler(ResourceStateConflictException.class)
    public ProblemDetail handleResourceStateConflict(ResourceStateConflictException ex) {
        ProblemDetail problem = ProblemDetail.forStatusAndDetail(
                HttpStatus.CONFLICT,
                ex.getMessage()
        );
        
        problem.setTitle("Resource State Conflict");
        problem.setType(URI.create(BASE_ERROR_URI + "state-conflict"));
        problem.setProperty("code", "INVALID_STATE_TRANSITION");
        problem.setProperty("resourceId", ex.getResourceId());
        problem.setProperty("currentState", ex.getCurrentState());
        problem.setProperty("timestamp", Instant.now());
        
        return problem;
    }

    @ExceptionHandler(BusinessRuleViolationException.class)
    public ProblemDetail handleBusinessRuleViolation(BusinessRuleViolationException ex) {
        ProblemDetail problem = ProblemDetail.forStatusAndDetail(
                HttpStatus.UNPROCESSABLE_ENTITY,
                ex.getMessage()
        );
        
        problem.setTitle("Unprocessable Entity");
        problem.setType(URI.create(BASE_ERROR_URI + "business-rule-violation"));
        problem.setProperty("code", ex.getErrorCode());
        problem.setProperty("timestamp", Instant.now());
        
        return problem;
    }
}

Step 3: Observe the Canonical RFC 9457 Wire Responses

With this implementation, your API produces consistent, informative error responses:

409 Conflict (State Machine Collision)

HTTP/1.1 409 Conflict
Content-Type: application/problem+json

{
  "type": "https://api.coffeestream.com/errors/state-conflict",
  "title": "Resource State Conflict",
  "status": 409,
  "detail": "Order 'a0eebc99-9c0b-4ef8' cannot be cancelled because it is in state 'SHIPPED'.",
  "instance": "/api/v1/orders/a0eebc99-9c0b-4ef8/cancellations",
  "code": "INVALID_STATE_TRANSITION",
  "resourceId": "a0eebc99-9c0b-4ef8",
  "currentState": "SHIPPED",
  "timestamp": "2026-08-04T09:30:00Z"
}

422 Unprocessable Entity (Domain Business Failure)

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json

{
  "type": "https://api.coffeestream.com/errors/business-rule-violation",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "Selected coffee roast 'Ethiopian Yirgacheffe' is currently out of stock.",
  "instance": "/api/v1/subscriptions",
  "code": "ITEM_OUT_OF_STOCK",
  "timestamp": "2026-08-04T09:30:00Z"
}

Best Practices for Enterprise Error Governance

1. Enforce Media Type Standards

Always return Content-Type: application/problem+json for 4xx and 5xx responses to conform with RFC 9457. This signals to clients that your error responses follow the standard format.

2. Standardize Machine-Readable Error Codes

Include a string identifier (like code: "ITEM_OUT_OF_STOCK") alongside human-readable detail messages. This allows frontend applications and API clients to handle errors programmatically without relying on fragile string-parsing of human messages.

3. Never Disclose Internal Stack Traces

Map internal database or framework exceptions to generic ProblemDetail objects without exposing internal table or class names. Your production errors should help clients debug their requests—not provide a roadmap to your database schema.

4. Preserve Distributed Trace Context

Add OpenTelemetry traceId and spanId as top-level extension properties in your ProblemDetail objects. This simple addition simplifies cross-service debugging and helps your operations team quickly pinpoint issues across your microservices architecture.


Wrapping Up

Thoughtful HTTP status code selection is a hallmark of well-designed APIs. By following this pragmatic guide and leveraging Spring Boot 3's RFC 9457 support, you can create enterprise-grade APIs that are consistent, debuggable, and developer-friendly.

The decision tree and matrix provided here serve as your compass when navigating ambiguous error scenarios. With these tools and a production-ready implementation, you'll no longer find yourself debating whether a request deserves a 400 or a 422—you'll know exactly which status code communicates the right message to your API consumers.

Tuesday, July 14, 2026

The Lost Promise of REST: Why Your API Isn't RESTful (and How to Fix It)

It is common in our industry to label any JSON-over-HTTP service as a "REST API." However, if we hold these services up to the standards set by Roy T. Fielding—the creator of the architectural style—the majority of them fail the test. Most are simply RPC-over-HTTP, or what I call "REST-ish."

They act as data-transfer services, but they lack the defining constraint that makes REST, well, REST: HATEOAS (Hypermedia as the Engine of Application State).

What Does HATEOAS Really Mean?

Fielding’s vision for REST was not about exposing database rows as URLs; it was about creating a system that acts like the web itself.

When you browse the web, you don't hardcode URLs into your browser's address bar to navigate a site. You read a page, see a link, and click it. You submit a form to change your state. The information on the screen provides the affordances—the cues—that tell you what you are allowed to do next.

As Fielding famously noted:

"When I say hypertext, I mean the simultaneous presentation of information and controls such that the information becomes the affordance through which the user (or automaton) obtains choices and selects actions."

For an API to be truly RESTful, machines must be able to follow these same cues.

Three Levels of Hypermedia-Driven Interaction

To move from "REST-ish" to truly RESTful, we must stop forcing clients to memorize our URL structures and start sending them instructions.

1. The Human Standard: HTML

We already know how to do this for humans. When a browser receives an HTML document, it doesn't need an API manual to know how to "Add to Cart." The <form> tag tells the browser the URL, the method, and the required fields.

HTML:
<form action="/cart/items" method="POST">
  <input type="hidden" name="product_id" value="prod_987">
  <button type="submit">Add to Cart</button>
</form>

The browser discovers the interaction, and the user executes it.

2. The Machine Standard: HAL+JSON

For machine-to-machine communication, we need a similar language. HAL (Hypertext Application Language) is a popular choice for expressing relationships. Instead of a static data block, the API returns a set of available links dynamically based on the resource's current state.

JSON:
{
  "order_id": "ord_1024",
  "status": "awaiting_payment",
  "_links": {
    "self": { "href": "/orders/ord_1024" },
    "payment": { "href": "/orders/ord_1024/pay", "title": "Submit payment" }
  }
}

If the order is already paid, the server simply omits the payment link. The client doesn't need if/else logic to guess if payment is allowed—it simply checks if the link exists.

3. The Intelligent Automaton: JSON-LD + Hydra

For highly decoupled systems, we can use JSON-LD paired with Hydra. This takes it a step further by providing a semantic vocabulary that tells the client exactly what is expected.

JSON:

{

  "@context": [

    "http://www.w3.org/ns/hydra/context.jsonld",

    { "@vocab": "http://schema.org/" }

  ],

  "@id": "/users/johndoe",

  "@type": "Person",

  "name": "John Doe",

  "email": "john@example.com",

  "operation": [

    {

      "@type": "ReplaceResourceOperation",

      "method": "PUT",

      "expects": "http://schema.org/Person",

      "title": "Update profile details"

    }

  ],

"operation": [
{ "@type": "ReplaceResourceOperation", "method": "PUT", "expects": "http://schema.org/Person", "title": "Update profile details" } ] }

Here, the client understands the context, the operation type, the HTTP method, and the expected data structure. No out-of-band documentation (like a PDF or an OpenAPI spec) is required to perform the update.

Why This Matters: The Shift to State Machines

When you build APIs this way, you change the nature of your client. Instead of a rigid script that breaks the moment you change a URL, your client becomes a dynamic state machine:

  • The Affordance is King: If the user has permission to edit, the server sends an edit link. If they don't, the link is absent.

  • No Hardcoded URLs: The client only needs the root URL. Every subsequent action is discovered through the payload.

  • Protocol-Driven: By using standard media types like application/hal+json, your API becomes self-documenting.

True REST isn't just about using HTTP verbs—it’s about letting the server drive the application state through hypermedia. It requires a shift in mindset, but the result is a system that is significantly more flexible, discoverable, and resilient to change.


How about REST and OpenAPI, can both technologies be combined?

To understand the relationship between REST (with HATEOAS) and OpenAPI, it helps to view them not as competing technologies, but as serving different functions in the lifecycle of an API.

The Core Difference: Implementation vs. Documentation

  • REST (with HATEOAS) is an architectural style. It defines how your API behaves and how a client interacts with it. HATEOAS (Hypermedia as the Engine of Application State) is the "glory" of REST, where the server provides links within the response to guide the client to the next possible states.

  • OpenAPI is a specification (a blueprint). It is a standard for describing an API. It is not the API itself; rather, it is a document that tells developers and machines what endpoints exist, what parameters they accept, and what they return.

FeatureREST (with HATEOAS)OpenAPI
PurposeDefines architectural constraints/behavior.Documents and describes the API.
FocusHow the client discovers state transitions via links.What the endpoints, schemas, and methods are.
Primary GoalDecoupling the client from the server's URL structure.Enabling automation, tooling, and developer onboarding.
StateThe server dictates the current state via hypermedia links.The document is static and describes potential states.

The Tension: Why They "Don't Work Together Nicely"

You may encounter friction when trying to combine them because their underlying philosophies can contradict each other:

  • REST/HATEOAS says: "The client shouldn't need to know the URL structure beforehand; it should just follow the links provided by the server".

  • OpenAPI says: "Here is a complete map of all URLs and structures so you can build your client right now".

When you use HATEOAS, you are intentionally trying to avoid "hardcoding" URL paths in the client. However, OpenAPI is designed specifically to document those exact paths. When you use an OpenAPI generator to create client code, it often generates static URL-building logic, which effectively defeats the purpose of the dynamic discovery that HATEOAS aims to provide.

Can They Coexist?

Yes, they are often used together, but it requires a careful approach:

  1. Documentation vs. Navigation: You can use OpenAPI to describe the "capabilities" of your API (the resources, the schemas, and the entry points) while using HATEOAS within the actual API responses to handle the specific navigational flow and dynamic state transitions.

  2. Extended Definitions: Some developers include custom link definitions within their OpenAPI files to represent the hypermedia relationships, though standard OpenAPI does not natively "force" the HATEOAS behavior.

  3. Framework Support: Libraries like springdoc-openapi-hateoas exist specifically to bridge this gap, attempting to help OpenAPI documentation recognize and represent the HATEOAS links that your code generates.

Summary: Think of the REST API as the building itself (its layout, its doors, and its hallways) and OpenAPI as the architectural blueprint or map you give to visitors. You can have a building that is designed for people to navigate by signs (HATEOAS), and you can still provide a map (OpenAPI) so they know generally where they are going—just be careful not to make your map so rigid that it breaks the navigation design of the building.