API Design Patterns

Exercise solutions

A lot of readers asked for solutions to the exercises in API Design Patterns, so here they are, free for everyone.

Each one goes further than the answer. I walk through the trade-offs between the possible designs, so you can see why a particular solution is right, or why several of them are. Have a go at the exercise first, then open the solution.

API Design Patterns by JJ Geewax

Chapter 3Naming

3.1Exercise 3.1

Imagine you need to create an API for managing recurring schedules ("This event happens once per month"). A senior engineer argues that storing a value for seconds between events is sufficient for all the use cases. Another engineer thinks that the API should provide different fields for various time units (e.g., seconds, minutes, hours, days, weeks, months, years). Which design covers the correct meanings of the intended functionality and is the better choice?

Show solutionHide solution2 min read

This is an interesting question in that it brings up a debate between specificity (e.g., the number of seconds between two recurring events) and usability (in this case, the user wanting to say "this event occurs every week"). But there's actually a bit more to it.

Let's start by looking at some options of how we might define this interface. The first option is a single field representing an interval of seconds, defining how many seconds should elapse before the next instance of the event:

interface RecurringEvent {
  name: string;
  intervalSeconds: Number;
  // ... more fields ...
}

Option 1: A single field defining the number of seconds between each recurring event.

The second option is to include additional units, such as minutes, hours, days, months, and years:

interface RecurringEvent {
  name: string;
  intervalSeconds: Number;
  intervalMinutes: Number;
  intervalMonths: Number;
}

Option 2: Several fields for each of the different recurrence interval units.

There's also a third option that relies on some sort of abbreviated syntax stored in a query-like string:

interface RecurringEvent {
  name: string;

  // s: seconds
  // m: minutes
  // mo: months
  interval: string; // E.g., "1s 2m 3mo"
}

Option 3: A query-like string to represent the recurrence interval.

So which of these is "the best"? Let's start by looking at the first option against a possible scenario: a monthly recurrence.

If we want something to recur on the first of each month, we actually run into quite a problem with the first solution. The issue is pretty simple: a single month doesn't have a fixed number of seconds. Some months (e.g., February) have 28 days (2,419,200 seconds) whereas others might have 31 days (2,678,400 seconds), so right off the bat, this API definition doesn't allow us to do something as simple as a recurrence on "the first of every month", which is a big problem.

When it comes to Option 2 and Option 3, we have more flexibility. For this specific example, it's pretty simple: either intervalMonths: 1 or interval: "1mo" and our problem is solved.

This might leave one big question unanswered: Why don't we have hours, days, weeks, years, or other time units involved? Why have minutes when that can just be 60 seconds? The answer is simple: we need a unit for every case where the lesser unit is inconsistent. For example, we need months because each month varies in the lesser units (seconds, minutes, or days). On the other hand, we don't need years because while each year might have a variable number of days (after all, leap years have an extra day), a year is always exactly 12 months. Finally we need minutes because there just so happens to be such a thing as a "leap second", where we add an extra second to one of the minutes at the end of the month to account for variations in the earth's rotation. In other words, not every minute is 60 seconds (some are 61 seconds)!

If I had to pick a standard, I'd choose Option 3, however I would add in aliases for common scenarios to avoid forcing users to do additional math. For example, rather than saying "If you want to define 1 week just say "10080m" to be 10,080 minutes!" I'd include an alias (1w) to handle that same scenario. It would be the same as "However many minutes specified + 10,080 more". The same goes for days, hours, and years.

interface RecurringEvent {
  name: string;

  // s: seconds
  // m: minutes
  // h: hours (equivalent to +60m)
  // d: days (equivalent to +1440m)
  // w: weeks (equivalent to +10080m)
  // mo: months
  // y: years (equivalent to +12mo)
  interval: string; // E.g., "1s 2m 3h 4d 5w 6m 7y"
}

And to interact with this, we might have some parsing code that looks like this:

interface Interval {
  seconds: number;
  minutes: number;
  months: number;
}

function parseIntervalString(intervalString: string): Interval {
    const interval: Interval = { seconds: 0, minutes: 0, months: 0};
    const rawInterval: Map<String, number> = new Map();
    const components = intervalString.trim().split(/ +/);
    for (let component of components) {
        let match = component.match(/^([0-9]+)(s|m|h|d|w|mo|y)$/);
        if (match == null) throw new Error(`Invalid interval ("${component}").`)
        let unit = match[2], value = parseInt(match[1]);
        if (rawInterval.has(match[2])) throw new Error(`Invalid interval. Duplicate unit "${unit}".`)
        rawInterval.set(unit, value);

        if (unit == 's') interval.seconds += value;
        else if (unit == 'm') interval.minutes += value;
        else if (unit == 'h') interval.minutes += 60*value;
        else if (unit == 'd') interval.minutes += 60*24*value;
        else if (unit == 'w') interval.minutes += 60*24*7*value;
        else if (unit == 'mo') interval.months += value;
        else if (unit == 'y') interval.months += 12*value;
    }
    return interval;
}

3.2Exercise 3.2

In your company, storage systems use gigabytes as the unit of measurement (109 bytes). For example, when creating a shared folder, you can set the size to 10 gigabytes by setting sizeGB = 10. A new API is launching where networking throughput is measured in Gibibits (230 bits) and wants to set bandwidth limits in terms of Gibibits (e.g., bandwidthLimitGib = 1). Is this too subtle a difference and potentially confusing for users? Why or why not?

Show solutionHide solution2 min read

This is a tricky question, but ultimately comes down to consequences. Let's assume that users will make mistakes with these fields and assume Gib and Gb are the same. What will happen when they make this mistake? Will it be catastrophic? Or is it more of an inconvenience?

To better understand this, let's look at how many "bytes" are involved here:

Unit Abbreviation Amount (bytes)
Gibibit Gib 134,217,728 bytes
Gigabyte GB 1,000,000,000 bytes
Difference 865,782,272 bytes (~865 Megabytes)

As you can see here, there is quite a big difference per unit: 1 Gibibit is only ~135 Megabytes, whereas 1 Gigabyte is 1,000 Megabytes, about a 7x difference for the two units. If we were using these both for storage values (e.g., sizeGB as well as sizeGib), this would be a huge problem. Can you imagine asking thinking you're asking for 500 GB and actually getting ~70 GB? Or asking for 500 GB and getting 3.5 TB? That might be a bit more than an inconvenience - it could be a costly mistake.

The subtlety of the problem arises when we realize that we're using one unit as the standard for network bandwidth and the other as the standard for storage space. But is this really a problem? Should we prohibit similar units to different concepts simply because they're similar? Or because they're measuring the same base unit (in this case, bits) in different group units?

Taking this to an absurd degree, it's a bit like asking whether we should avoid using ageYears because we use appointmentDurationMinutes elsewhere in the API . The units apply to entirely different concepts, domains, and scenarios, so it seems unreasonable in this absurd example to not use the right unit for the job, despite both measuring a base unit (in this case time, so... seconds?). Yes, the naming is more subtle (just a single letter difference), but assuming these are the industry standards for measuring these values (Gibibits for network bandwidth, Gigabytes for storage space), this should be the basic assumption by users.

In fact, it might be more confusing, for example, to measure network bandwidth in Gigabytes and lead to users allocating ~7x more bandwidth than they intended by setting networkBandwidthLimitGB=10 expecting a 10 Gibibit limit but actually getting a ~70 Gibibit limit because they actually set a 10 Gigabyte limit.

In short, this is a tough one, but if I had to choose, I'd say it's fine to have these two fields using these two units. This is based on the fact that the values are fundamentally different (storage space versus network bandwidth) and each value uses the industry standard unit.

Chapter 4Resource scope and hierarchy

4.1Exercise 4.1

Imagine you're building a bookmark API where you have bookmarks for specific URLs online and can arrange these in different folders. Do you model this as two separate resources (Bookmark and Folder) or a single resource (e.g., Entity), with a type in-line to say whether it's acting as a folder or a bookmark?

Show solutionHide solution1 min read

💡 Fun fact: This exercise is one of the ones we used to debate in Google's API Design Class. It's fascinating how many unique answers there are for the resource layout of something so seemingly simple!

First, let's look at the two options being proposed. First, we have two separate resources (Bookmark and Folder), each with their own identifier.

interface Bookmark {
  id: string;
  url: string;
  folderId?: string;
}

interface Folder {
  id: string;
  displayName: string;
  folderId?: string;
}

Option 1: Two separate resources (Bookmark and Folder)

The other is a more generic design, using polymorphism (see Chapter 16: Polymorphism) with a single resource (Entity).

interface Entity {
  id: string;
  type: "folder" | "bookmark";
  folderId?: string;
  details: BookmarkDetails | FolderDetails;
}

interface BookmarkDetails {
  url: string;
}

interface FolderDetails {
  displayName: string;
}

Option 2: A single polymorphic resource (Entity)

Option 1 uses a standard referential relationship, where each Bookmark resource belongs to a single Folder resource. Option 2 uses a single Entity resource with a self reference that points to another Entity resource. This works out quite well because both "types" can live inside folders (i.e., this supports nested folders).

So why choose one over the other?

One big benefit of using a single Entity resource (even if the name needs some work) is that you can list all of the data with a single API call like ListEntities(). You can take the output of this API call and build your bookmarks menu. This seems to align quite well with the stated goal of a bookmarks API.

On the other hand, if we have two distinct resource types, this means that since a folder can contain both bookmarks and other folders, in order to build a menu we'll have to make two separate API calls (ListBookmarks() and ListFolders()), often making one after another for each folder that we run into (ListBookmarks(filter="folder:1234")), which means quite a bit more traffic on the wire.

While this question seems to be more about the polymorphic aspect of the resource layout rather than the relational aspect, it's important to note that both options are perfectly valid (using either a standard reference relationship or a self-reference relationship). In the use case at hand though it seems that the polymorphic resource (Entity) is the better candidate.

4.2Exercise 4.2

Come up with an example and draw an entity relationship diagram that involves a many-to-many relationship, a one-to-many relationship, a one-to-one relationship, and an optional one-to-many relationship. Be sure to use the proper symbols for the connectors.

Show solutionHide solution3 min read

💁‍♂️ There are many tools for rendering entity relationship diagrams. In this article we'll talk about using Mermaid, which just so happens to be how you would do this on GitHub with their flavor of Markdown.

For this example, let's imagine that we're building an online store with the following resources:

  • Customer represents an individual placing orders in the store.
  • BillingMethod is the billing information (credit card number, address, etc) for a Customer.
  • Order represents a purchase made by a Customer.
  • Product is an item available for purchase in an Order.

One-to-many relationship

Before a customer can place any orders, they first need the input their billing information which might include things such as a credit card number and billing address. Just like Amazon, customers should be able to store lots of different billing methods (as they might have more than one credit card).

Since customers might have multiple billing methods (and at least one on file) this is a simple (non-optional) one-to-many relationship (each Customer has 1 or more BillingMethod resources and each BillingMethod is always owned by a single Customer).

ℹ️ In Mermaid, this relationship is modeled using ||--|{ . For example, a Customer may have one or more BillingMethods, while a BillingMethod has exactly one Customer would be modeled as Customer ||--o{ BillingMethod.

One-to-one relationship

Similar to our one-to-many relationship, we actually have a similar one-to-one relationship in the "default" billing method. In order to place orders quickly, customers must have exactly one default billing method on file (which can be changed to a different billing method at any time).

Since a customer should have only one default billing address, credit card, etc, this is a strict one-to-one relationship (each Customer has exactly one default BillingMethod resource, and each BillingMethod always points to exactly one Customer).

ℹ️ In Mermaid, this relationship is modeled using ||--|| . For example, a Customer will have exactly one BillingMethod (and vice versa) would be modeled as Customer ||--|| BillingMethod.

Optional one-to-many relationship

An optional one-to-many relationship is present between customers and orders. Customers might place lots of orders (or none, hence the "optional"), whereas a given order must always be placed by a single customer.

ℹ️ In Mermaid, this relationship is modeled using ||--o{ . For example, a Customer may place 0 or more Order resources (and an Order always belongs to a single Customer) would be modeled as Customer ||--o{ Order.

Many-to-many relationships

When a customer places an order, that order must have at least one product being ordered, but more likely even more than one. Similarly, there are many different products being sold and each one might be part of lots of different orders (e.g., Customer 1 orders a book, Customer 2 orders the same book).

Since an Order is composed of 1 or more Product resources, and a Product can be referenced in many different Order resources (or none), we have a many-to-many relationship. This relationship also happens to be optional on one side.

ℹ️ In Mermaid, this relationship is modeled using }o--|{. For example, a Order may have 1 or more Product resources being purchased, and a Product might be part of 0, 1, or many different Order resources. This would be modeled as Order }o--|{ Product.

Final entity relationship diagram

Thanks to the magic of Mermaid we can easily generate an ERD from some text. Below you can see the diagram source as well as the rendered ERD.

👷‍♂️ There are lots of ways to render these diagrams. One easy one is to use Mermaid Live Editor which also has handy buttons to download and copy the rendered diagrams in both PNG and SVG format.

erDiagram
    Customer ||--|{ BillingMethod : "owns"
    Customer ||--|| BillingMethod: "has default"
    Customer ||--o{ Order : "places"
    Order }o--|{ Product : "has"

Mermaid diagram source of all relationships