Safety by Discipline and Safety by API Design

I love languages with a type system and compilation—the confidence of “if it compiles, it’ll work” is worth every penny (<3 Golang)

I first heard about these terms when I was in college, and it has been something I always think about when designing systems. Essentially, it’s a derivation of the age old programming adage “Premature optimization is the root of all evil”, but that is vague and subjective. I find this clearer in context.

Let’s take an example: you’re building a B2B application, and (since you hopefully won’t sell it to only one company) you need to add support for multi-tenancy.

Most people start with a single database that includes a tenant key like workspace_id or organization_id to scope data to that account. You build the application, and you write all your queries so that every query includes a WHERE clause on the tenant key. For someone building a new feature, they need to ensure that any new queries they add contain the tenant key condition; otherwise, you’ll leak data across tenants (!!!).

If someone makes a mistake, it might get caught during code reviews, testing, staging deployments, etc., and the system will keep working fine for a long time.

It’s possible the system never breaks, but the cognitive load of maintaining this setup slowly nibbles away at your speed and productivity. It’s just one more thing to keep in mind. This is safety by discipline—it works most of the time, until it doesn’t.

Now, let’s say you bind the tenant key to the user’s session and automatically add the tenant key at the database session or middleware level. This way, other parts of the code don’t need to worry about scoping queries to a tenant. In this case, you’ve made the tenant system safe by design.

You don’t have to check every SQL query to see if it’s scoped to the tenant. If a developer uses the abstractions you designed for querying, you know their code is safe.

This is just one example I picked from a recent project I worked on. This concept applies anywhere you’re building an API—i.e., exposing some functionality via an abstraction. It could be building a React component or a class in your backend code for a third-party integration.

Building strict ways your APIs can be used makes it much easier to manage and gives you peace of mind. But, like all things, this comes with trade-offs—you may end up compromising on flexibility. If something has a specific way it can be used, it automatically limits the number of ways it can be extended.

The general approach I take is: if I don’t know enough about a system or how it will evolve, I go with the safety by discipline approach. This ensures I don’t build bad abstractions too early. Once I’ve worked on the system and have a pretty good understanding of how it’s going to evolve, I spend time designing a good abstraction that reduces the number of mistakes any consumer could make.