Why you shouldn’t use booleans in REST APIs

Hany Elemary
Geek Culture
Published in
7 min readJun 16, 2021

--

When designing REST APIs, convenient decisions can negatively impact the evolution of our work. An example of this is using booleans in API contracts. If not carefully considered, booleans can:

  • Obstruct API Extensibility
  • Mask and obfuscate Domain Clarity
  • Hamper Code Readability and Maintainability

Let’s dig into these areas and audit how booleans are commonly used in REST APIs.

API Extensibility

An extensible API should make future changes obvious to think about and easy to enact. It should not introduce complexity or unnecessary duplication or break existing consumers.

Example: Github API — Create new repository

Consider Github’s REST API. To create a new repository, developers specify whether the repository is private by setting a boolean field (default: false). This is one of the common uses of booleans — a single state indicator for an HTTP resource (repos).

HTTP Request: POST /user/repos
{
"name": "Hello-World",
"description": "This is your first repository",
"private": false,
...
}

This sample POST request seems straightforward at first. But when Github later introduced Organization repositories, they needed to restrict access to only-members of organizations. In this case, the boolean field private was no longer sufficient. So they added a new visibility enum with the following options:

  • internal — restricted to members of an organization
  • public— publicly accessible from the internet
  • private— restricted to repository creators and collaborators

With that change, the documentation states that visibility overrides private when both are used. Had Github used the visibility enum initially with two possibilities ['public', 'private'], it would have been more obvious to add the third (internal) without introducing new fields. While this change didn’t break existing consumers, it contributed to confounding the documentation and duplicating domain concepts in the API.

Now let’s see another common use of booleans in APIs — Capability Configuration for an HTTP resource. In this case, a group of booleans work together to provide a set of capabilities that are either turned on or off. Using the Github endpoint to create a repository, the request looks like this:

HTTP Request: POST /user/repos
{
"name": "Hello-World",
"description": "This is your first repository",
"private": false,
"has_issues": true,
"has_projects": true,
"has_wiki": false,
"has_downloads": true

}

Here, the booleans has_issues, has_projects, has_wiki, has_downloads represent capabilities users will see once a repository is created. In this example, the client code will have a separate execution flow for each boolean that’s truthy. Here is a diagram representing this flow:

Capability Configuration booleans

The problem here is that the 4 booleans produced 16 different permutations to test in client code. This is described by an exponential complexity function of 2^n where n is the number of booleans, and 2 is the binary value true or false. Let’s say a new, related user feature is needed conditionally. Adding a new boolean will produce 32 different permutations to test.

This exponential complexity violates the definition of extensibility introduced earlier. A better design would be to turn this configuration into a mutually inclusive data structure such as an array repository_capabilities: ["has_issues", "has_projects", "has_downloads"]. This reduces the different paths since API consumers can use a map of capability to execution flow to build user functionality dynamically.

Domain Clarity

A clear domain should be instructive. Its components should be discoverable via a ubiquitous language. It should encapsulate logic via clear boundaries to avoid downstream bugs. And, lastly, it should make it hard to misuse any of its components.

Example: Payments API — Send new payment

Let’s consider an example in the financial services industry. Most payment gateway APIs will respond with a boolean field isApproved to a payment request. This is a single state indicator for an HTTP resource (/payments).

HTTP Request: POST /payments
{
"transactionId": "uniqueId",
"paymentDetails": {...}
"chargeAmount": 15.00,
 ...
}
HTTP Response:
{
"isApproved": true,
"approvedAmount": 15.00,
...
}

At first glance, this API contract makes sense. A customer sends a payment, then the payment is either approved or declined. But as you dig deeper, you find masked concepts for different approval outcomes besides the binary result isApproved.

To elaborate on this, let’s say you use a Debit Card having a balance of $10 to pay for a transaction amount of $15. Depending on the merchant’s configuration, the transaction may be approved for the balance of $10. This is a Partial Approval where the merchant gladly accepts your $10 and asks for the remainder via cash or another card¹. Here is a sample API contract for this scenario:

HTTP Request: POST /payments/
{
"transactionId": "uniqueId",
"paymentDetails": {...}
"chargeAmount": 15.00,
...
}
HTTP Response:
{
"isApproved": true,
"approvedAmount": 10.00,
...
}

In this API, the concept of “Partial Approvals” is implicit. API consumers have to check that 1) the payment isApproved and 2) the approvedAmount is less than the initial chargeAmount. The implicit nature here is a symptom of the domain logic leaking out to consumers. In other words, this forces consumers to figure out what the logic for being “Partially Approved” is, rather than us telling them directly. This lack of clarity around domain boundaries also couples consumers to domain logic.

Now let’s see how this example impacts code readability and maintainability.

Code Readability and Maintainability

Code readability isn’t merely about reading what the code does. Revealing the intent of the code is what’s important — why the code does what it does.

In the previous examples, the implicit nature of “Partial Approvals” in the API nudges API consumers to write this code:

Here, “Partial Approvals” for payments are implicit

There is a better way, however. If we introduce “Partial Approvals” explicitly in the API, we can write code that reflects the ubiquitous language of the domain — code that clearly reveals our intent. Here is the recommended API:

HTTP Request: POST /payments/
{
"transactionId": "uniqueId",
"paymentDetails": {...},
"chargeAmount": 15.00,
...
}
HTTP Response:
{
"paymentStatus": "PARTIALLY_APPROVED"
"
approvedAmount": 10.00,
"remainingAmount": 5.00,
...
}

Now our API has an enum paymentStatus with the value PARTIALLY_APPROVED. This intentional clarity in the API pushes developers to write code reflecting the API language:

Here, “Partial Approvals” for payments are explicit

By using an enum, we allow API consumers to 1) discover different values via the API itself and, therefore, learn more about the domain and 2) benefit from its strongly-typed values which, in turn, reduce bugs (i.e: rounding errors).

Let’s consider one last example of boolean usage in APIs — Policy Execution on an HTTP resource. The difference between this and Capability Configuration booleans is that the client may not care so much about permutations. Instead, the client may only care about what is allowed and ignores anything else outside of that. The execution flow remains more or less the same. Let’s see an example.

When contactless payments (Google Pay, Apple Pay, etc) were introduced, merchants needed to adjust their configuration to accept them. This configuration change may happen via APIs — often incorrectly via booleans. Here is a sample GET request for this configuration.

HTTP Request: GET /store/{id}/configuration
{
"allow_apple_pay": true,
"allow_google_pay": true,
"allow_samsung_pay": false,
...
}

You’ll see here a list of allowed payment methods for a store’s configuration. Yet the end result is the same — process payments from allowed methods and decline them if they’re not supported. Here is a diagram for this Policy Execution example of booleans:

Policy Execution booleans

This API response nudges developers to write code that looks like this:

These different permutations do not just impact code maintainability, they make testing the code also more awkward and unnecessarily repetitive. Now let’s change our API response to return an array of allowedPaymentMethods as such:

HTTP Request: GET /store/{id}/configuration
{
"allowedPaymentMethods": [
"GOOGLE_PAY",
"APPLE_PAY"
],
...
}

This simple change will have a huge maintainability advantage over the code. Our code will now look something like this:

If new payment methods are introduced and become allowed for a given store, this piece of code doesn’t need to change.

When to use booleans in APIs

There are situations in which we can benefit from that rigid nature of booleans in REST APIs. However, these situations should be guided by the constraints of the domain itself.

Example: Wallet API — Add Payment Option.

To illustrate, let’s consider a digital Wallet API. A wallet includes different payment options for cards (i.e: Credit, Debit, Gift Card) or payment methods (i.e: Google Pay). Wallet APIs account for a default payment so users don’t have to select the same card on every transaction. Here is an example:

HTTP Request: POST /wallet/payment-options
{
"maskedNumber": "*******4433"
"cardNetwork": "VISA",
"nickname": "My Preferred Card",
"default": true,
...
}

Since paying for a single transaction online with multiple cards at the same time isn’t possible², it’s better to use a boolean that resembles the domain’s limitations.

Now let’s imagine that funding a transaction from 2 separate payment options becomes possible. Is the concept of default still valid with this change? As it turns out, it still is. In this case, users may have to pick another card on top of their default.

So our design intent was best communicated via the strict nature of booleans. Using booleans 1) didn’t hold us back from extending API functionality, 2) didn’t obfuscate the domain and, therefore, 3) won’t impact the readability of our code.

Closing thoughts

  • APIs need to strike a balance between strictness and flexibility. Booleans are restrictive by definition.
  • APIs should be instructive. The domain language should shine through their contract. And possible values of each field in the contract should be easily discoverable.
  • Booleans don’t only pose issues for REST APIs. Here is a good read on the problems with flag arguments by Martin Fowler.
  • Enums and arrays afford us far more flexibility in API design than booleans. Check out Google’s API design patterns for boolean vs enum vs string.

Acknowledgments

Big thanks to Megan Lusher and Smitha Ajay for reading drafts of this article and providing feedback on improvements.

¹ This mostly happens for in-person/in-store transactions (aka: card-present transactions). If the transaction happens online, the card might be outright declined. There are other scenarios such as over-drafting that could occur depending on customers and banks.

² There are very rare cases when a merchant allows customers to use a combination of a credit/debit card and their own issued gift card. This is certainly an exception and is not a common flow.

--

--

Hany Elemary
Geek Culture

Technology Leader. High Performing Teams Enabler. Author & Speaker.