Skip to main content

Should HTTP Status Codes Reflect Business Outcomes?

I’ve delivered my Beyond the Code: Services Which Stand the Test of Time presentation a handful of times this year. Toward the end of the first half, I take a stronger stance and talk more directly about the design of RESTful interfaces.

Most of the material in the presentation is framed as guidelines rather than hard rules. However, the RESTful section leans much more strongly toward rules, and there’s one rule in particular that I consider non-negotiable:

Never, EVER, use a field in a response to indicate success!

Then I show this example on the next slide of why you shouldn’t do it:

Just in case it isn’t immediately obvious, there is a conflict here between the HTTP status code of 200, indicating everything is OK, and the success field in the response body saying that it isn’t. Usually this gets a knowing, sometimes uncomfortable laugh from the audience, but not tonight in Digital Colchester.

One audience member challenged this stance with a question I’ve been thinking about since I first wrote my RESTful Behaviour Guide: am I coupling the HTTP request with the business capability the service delivers?

HTTP Requests and Business Capabilities

To explore that question, it helps to separate two concerns that are often conflated.

An HTTP request and a business capability represent two distinct concerns. An HTTP request covers the technical interaction with a service: whether the request reached the service, was understood, and whether the handler executed without error. By contrast, a business capability reflects the outcome of the domain-specific operation the request was intended to perform, such as processing a payment, placing an order, or validating a user’s eligibility.

From this perspective, a request can succeed, for example, it arrives successfully and is handled by the service without error, while the underlying business action fails for entirely different reasons. A credit card might be declined, an order might already have been shipped, or a user might not meet certain criteria.

The argument, then, is that HTTP status codes should report only on the success or failure of the request itself, while the response body should communicate the business outcome. Combining both concerns into a single signal risks losing important information. It’s a reasonable position, but it relies on one key assumption: that HTTP semantics actually support such a clear separation.

The answer depends on what you think a 2xx status code actually means.

What the Specification Says

The answer isn’t subjective, it’s defined in the HTTP specification.

RFC 9110, the current HTTP semantics specification, defines the 2xx range as (15.3):

The request was successfully received, understood, and accepted.

The word accepted is what’s important here. HTTP is an application protocol, not a transport protocol. A 2xx doesn’t just mean that the request arrived, it means that the service agreed to do what you asked. This shouldn’t be confused with a 202 Accepted response, which means the service is going to process the request asynchronously. If the business rule rejected the request, the server didn't accept it. It understood it, but declined to fulfil it. That's 4xx according to the specification.

In practice, most of these cases are already well covered by existing 4xx status codes:

  • 422 Unprocessable Content - Syntactically valid, semantically understood, but the business rules say no. The canonical "your request was fine, the answer is no."
  • 409 Conflict - State-based rejection. "Order already shipped." "Email already taken."
  • 403 Forbidden - The caller isn't allowed to do this.
  • 402 Payment Required - Increasingly used for billing and quota outcomes. Stripe returns this for a card decline.

Instead of inventing your own error envelope, RFC 9457 - Problem Details for HTTP APIs,  provides a standard body shape for failures, consisting of type, title, detail, and status fields.

Adding an additional indicator of success to the response body adds no value.

Business Capability Response

That said, not every negative outcome is an error.

For query-style endpoints, a negative result doesn’t automatically mean something went wrong. A call to GET /eligibility/{user} may get back {eligible: false}, and that’s still a perfectly valid, successful response. The question was “is this user eligible?” and the system answered it. There’s no error in the request or the processing. The outcome just happens to be “no.” In cases like that, a 200 fits, because the API did exactly what it was asked to do; the business outcome is the resource.

The same applies to decision-as-a-service style endpoints. A fraud check returning {decision: "decline", reason: "..."} hasn’t failed, it has done exactly what it was designed to do. Declining is a normal, expected outcome of that service, not an exceptional condition. From the API’s perspective, it evaluated the request and produced a result, which is about as successful as it gets.

The Exceptions

There are, however, some genuine exceptions, cases where returning a 2xx alongside outcome details in the response body does make sense.

If you set pure REST concerns aside, GraphQL is the obvious example. By design, it almost always returns HTTP 200 unless there’s a transport-level failure, and pushes errors down into the response body instead. That’s because a GraphQL query can be partially successful, so a single HTTP status code can’t reliably capture the full outcome.

Of course a 207 Multi-Status response is the REST equivalent.

Finally

The more I’ve reflected on that audience question, the clearer the distinction has become. HTTP status codes are not just transport signals; they are part of the application contract. A 2xx response means the request was accepted in a meaningful sense, not just received without error. When a system refuses to carry out what was asked, whether due to validation, state, permissions, or business rules, that refusal should be expressed through the status code, not hidden in the response body.

At the same time, not every negative outcome is a failure. When an endpoint exists to answer a question or produce a decision, responses like false or decline are valid results, not errors. The key is to distinguish between a result of the operation and a rejection of the request. Keep status codes focused on the request, keep the body focused on the outcome, and avoid duplicating intent across both. That’s why the rule still stands: you don’t need a success field, and adding one only introduces ambiguity where HTTP already provides clarity.


Comments

Popular posts from this blog

Write Your Own Load Balancer: A worked Example

I was out walking with a techie friend of mine I’d not seen for a while and he asked me if I’d written anything recently. I hadn’t, other than an article on data sharing a few months before and I realised I was missing it. Well, not the writing itself, but the end result. In the last few weeks, another friend of mine, John Cricket , has been setting weekly code challenges via linkedin and his new website, https://codingchallenges.fyi/ . They were all quite interesting, but one in particular on writing load balancers appealed, so I thought I’d kill two birds with one stone and write up a worked example. You’ll find my worked example below. The challenge itself is italics and voice is that of John Crickets. The Coding Challenge https://codingchallenges.fyi/challenges/challenge-load-balancer/ Write Your Own Load Balancer This challenge is to build your own application layer load balancer. A load balancer sits in front of a group of servers and routes client requests across all of the serv...

Catalina-Ant for Tomcat 7

I recently upgraded from Tomcat 6 to Tomcat 7 and all of my Ant deployment scripts stopped working. I eventually worked out why and made the necessary changes, but there doesn’t seem to be a complete description of how to use Catalina-Ant for Tomcat 7 on the web so I thought I'd write one. To start with, make sure Tomcat manager is configured for use by Catalina-Ant. Make sure that manager-script is included in the roles for one of the users in TOMCAT_HOME/conf/tomcat-users.xml . For example: <tomcat-users> <user name="admin" password="s3cr£t" roles="manager-gui, manager-script "/> </tomcat-users> Catalina-Ant for Tomcat 6 was encapsulated within a single JAR file. Catalina-Ant for Tomcat 7 requires four JAR files. One from TOMCAT_HOME/bin : tomcat-juli.jar and three from TOMCAT_HOME/lib: catalina-ant.jar tomcat-coyote.jar tomcat-util.jar There are at least three ways of making the JARs available to Ant: Copy the JARs into th...

Do software engineering professionals still read? - survey results

  In order to gauge the potential audience for my book, So you think you can lead a team? , I conducted a small survey of my colleagues, co-workers and anyone from Linked. I read regularly, for work and pleasure, and assumed everyone else did too but did the responses I received confirm this? I polled 173 people, all within the software engineering field (including Product, etc), with a range of ages and years of experience in their role. What surprised me the most was that the majority of people, young or old, just starting or seasoned, still prefer reading physical books to blogs or e-readers. It also seemed that the older and more experienced were the most keen in learning more, and reading to expand or update their knowledge.  When it comes to reading habits between different roles the survey showed that software engineers and team leads read more regularly for their career than other roles, with 55 years old and over and 16+ years experience being the biggest readers over...