# Stop Designing Starbucks APIs: Defaults, Options & Complexity

# Stop Designing Starbucks APIs: Defaults, Options & Complexity

I was re-reading John Ousterhout’s *A Philosophy of Software Design* recently. There’s a definition that's been resonating in my head for a few years now:

> “Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.”

For most experienced engineers, that line is obvious.

What actually stuck with me this time was something else that Ousterhout points out: abstractions mostly fail in just two ways. Either they include unimportant details, which increases cognitive load, or they omit important details, which creates obscurity. And of course, obscurity feeds back into cognitive load, because you end up reverse-engineering what the system is doing instead of just using it.

Once you see complexity that way, as a choice between drowning people in irrelevant detail or hiding what actually matters, it’s hard not to notice the same pattern everywhere. Even coffee shops become a lesson in API design.

If your API feels like ordering at a trendy coffee shop with a 50-line menu, you don’t have a flexibility problem, you have a complexity problem. Good architecture hides that behind clean interfaces and lets options appear only when someone explicitly asks for more control.

This post uses that lens (cognitive load vs. obscurity) to look at API design, and coffee shops happen to be a pretty good concrete metaphor that makes the idea easier to think about.

## Cognitive Load vs. Obscurity in Software (and What Coffee Shops Have to Do With It)

Let’s make Ousterhout’s two failure modes concrete.

Cognitive load is the extra mental effort you need just to understand how to use a module, not to solve your actual problem. Things are visible, maybe even configurable, but you can’t move without understanding a pile of knobs and flags.

Obscurity is when the important decisions in a system are hidden from you. The system does “something”, but you can’t tell what or why, and you have no reasonable way to influence it.

Now forget software for a minute. Think about coffee shops.

## The Cheap Bar: Convenience with Zero Control

In some places, ordering coffee is brutally simple. You walk in and there are basically two options: “coffee” or “coffee with milk”. That's it. It’s actually convenient for 80% of people: you get something “good enough” without making a single decision, but you also have almost no control. If you care even a little bit about how your coffee tastes, you’re out of luck. The “interface” is simple, but only because it refuses to expose any meaningful choices. A lot of “simple” software interfaces are aiming for exactly this: convenience for the majority, at the cost of flexibility for everyone else.

This means low cognitive load (you don’t have to think), but high obscurity (you can’t see or influence what matters).

## The Starbucks Menu: “Flexibility” as Cognitive Load

Then you have the other extreme: the trendy coffee shop with a 50-line menu. You can choose size, roast, number of shots, type of milk, temperature, sweetness level, toppings, seasonal extras, and three different kinds of foam. Go into one of these places and just ask for “a coffee with milk”. I dare you.

On paper, this is “flexible”. In reality, it’s pushing every internal choice of the business onto the customer. If you just want “a normal coffee”, you’re forced to care about decisions you never wanted to face.

This is low obscurity (everything is visible), but huge cognitive load (you have to care about way too much).

## The Decent Café: Defaults First, Options on Demand

The best coffee shops sit somewhere in the middle. If you walk in half-asleep on a Monday and just say “a latte, please”, you get a good, default version without answering a questionnaire.

But if you care about oat milk, an extra shot, or less foam, you can ask for it. The options are there, but they don’t attack you upfront. The default path is boringly simple; the extra complexity only appears when you go looking for it.

That “decent café” is the mental model we want for our APIs.

## From Coffee Shop Menus to API Design

In software, we do the same thing with interfaces.

Some APIs expose almost no choices and hard-code everything. Others dump every possible option into a single method signature “for flexibility”.

In Ousterhout’s terms, the first kind omits important details and leaves you in the dark; the second kind includes a lot of unimportant details and overwhelms you with noise. And a few interfaces try to behave like that decent coffee shop: simple for the common case, but with a path to more control when you actually need it.

Let’s make this less abstract with a concrete example. I’ll use Java, but the shape of the problem is the same in any OO language.

## Cheap Bar APIs: Underpowered and Opaque

First, the “cheap bar” version of an email API:

```java
// “Cheap bar” interface: almost no control
EmailService.send(String to, String subject, String body);
```

It’s easy to call: great for demos, great for trivial scripts, but it’s useless the moment you care about anything slightly non-trivial:

* who is the email from?
    
* what happens on failure?
    
* what about retries, timeouts, localization?
    
* can you control headers, attachments, tracking?
    

You have low cognitive load (it’s easy to remember how to call it), but very high obscurity: all the important decisions are buried somewhere inside EmailService, and you have no say in any of them. In Ousterhout’s terms, this kind of interface omits important details and creates obscurity: you can’t see or control what actually matters.

The usual escape route from here is ugly: you start adding increasingly specific variants like:

```java
sendWithCc(...)
sendHighPriority(...)
sendWithAttachments(...)
```

You haven’t solved obscurity; you’ve just spread it across more methods.

## Starbucks APIs: Over-Parameterized and Exhausting

Now the Starbucks version:

```java
// “Starbucks” interface: every possible option as a parameter
void sendEmail(
    String from,
    String to,
    List<String> cc,
    List<String> bcc,
    String subject,
    String body,
    boolean highPriority,
    boolean trackOpens,
    boolean trackClicks,
    int retryCount,
    Duration timeout,
    boolean async,
    Locale locale
);
```

On paper this is “flexible”. In theory, callers can do anything.

In practice, every piece of code that calls this method now has to make decisions about retries, timeouts, tracking, and localization just to send a basic message. You’ve taken all the internal decisions of the system and turned them into function parameters.

In Ousterhout’s terms, this abstraction includes a lot of unimportant details for most callers, which drives up cognitive load: people have to keep a bunch of irrelevant choices in their head just to make a simple call.

You’ve turned your API into one of those overcomplicated coffee menus.

## The Decent Café API: Defaults and Opt-In Complexity

Now let’s look at a “decent café” version:

```java
// “Decent café” interface: simple default, opt-in complexity
Email email = Email.to("user@example.com", "Subject", "Body");

// sane defaults for: from address, retry policy, timeout, locale, tracking…
email
    .withCc("manager@example.com")
    .highPriority()
    .trackOpens()
    .withRetryPolicy(RetryPolicy.aggressive())
    .send();
```

Here, the “normal coffee” path is boringly simple:

```java
Email.to("user@example.com", "Subject", "Body").send();
```

You still have a full set of behaviors inside the system: retries, timeouts, tracking, localization and so on. They’re just not forced on every caller. They’re available as options, not obligations.

Callers who don’t care about retries, timeouts or tracking don’t have to think about them.

Callers who do care can opt into more control using the same API, even if it requires understanding a more complex model.

You keep cognitive load low for the common case without creating obscurity for the advanced one. The complexity is still there, but it only shows up when you go looking for it.

You can push the same idea further with polymorphism: instead of adding boolean flags like aggressiveRetry = true, you hide that variation behind a RetryPolicy or a similar strategy object. The core interface stays small and stable; the complexity lives in interchangeable implementations.

That’s the “decent café” version of API design.

## A Simple Exercise for When Your API Feels Wrong

I don’t run a formal checklist every time I design an API. Most of us don’t.

But when an interface starts to feel like either the cheap bar or the Starbucks menu, there’s a simple exercise that helps.

First, sketch a boring, standard path: the minimal call that represents how a “normal” user of this module would use it. Don’t try to be clever, just write down what a realistic call should look like.

Then look at the design from both sides:

**From the cognitive load side:**  
For each piece of information you’re forcing the caller to provide, ask whether a sane default would work for most cases. If yes, maybe that belongs behind a default, in configuration, or in a separate “advanced” overload instead of in the standard call.

**From the obscurity side:**  
For each thing you didn’t expose, ask whether there is a realistic scenario where someone will need to change or customize it. If yes, consider exposing it, but as an option, not as another mandatory decision.

You don’t have to start from “too simple” and grow it, or from “too complex” and shrink it. You can be in both failure modes at once: asking for too much information while still hiding important decisions.

The point of the exercise is to move a few things from visible → default, and a few others from hidden → option, until the trade-off feels intentional.

If your default call still reads like a Starbucks order screen, you don’t have a flexibility problem, you have a complexity problem.

And if your API feels like ordering at a trendy coffee shop with a 50-line menu, you already know which side you’ve chosen.
