Skip to content

Archive

API Design

35 articles
Software Engineering 04 Sep 2026 8 min read

Replacing Boolean Parameters with Explicit Operations

A boolean parameter can make an API compact while making each call harder to understand. Consider this call: save(document, true) What does true mean? Overwrite an existing document? Validate before saving? Publish immediately? The caller knows only if they remember the parameter definition or inspect the function.

Software Engineering 04 Sep 2026 8 min read

Removing Temporal Coupling from APIs

Some APIs look simple because each method is simple. The difficulty appears only when you try to use them: configure() must run before connect(), connect() before start(), and stop() is valid only after startup succeeds. When correctness depends on operations happening in a particular order, the code has temporal coupling. The word temporal refers to time or sequence: one operation is valid only because another operation happened earlier. Temporal coupling is not automatically a design flaw. Many real processes have genuine ordering constraints. The engineering problem is hidden temporal coupling: callers must remember an important sequence that the API does not make clear or enforce.

Software Engineering 03 Sep 2026 11 min read

Reducing Temporal Coupling in Code

Some APIs look simple because each method is simple. The difficulty appears only when the methods must be called in exactly the right order. A report builder might require loadData() before render(). A client might require connect() before send(). A job might require prepare() before execute() and execute() before publish(). When those rules exist but are not visible in the interface, callers must remember history: What has already happened to this object? That dependency on operation order is called temporal coupling.

Software Engineering 03 Sep 2026 8 min read

Changing Interfaces Safely with Parallel Change

Changing a shared interface often looks simple in the code that owns it. Rename a method, replace a parameter, or return a richer result, then update the callers. The difficulty appears when many callers cannot change at the same moment. They may live in different modules, be maintained by different teams, or be deployed independently. A change that is correct in isolation can then create a period where old callers and new code cannot work together.

Software Engineering 02 Sep 2026 5 min read

Parallel Change for Safe Interface Refactoring

Changing a shared interface is risky when many callers depend on it. A large “update everything at once” patch can work in a small codebase, but it becomes harder to review, deploy, and roll back as dependencies spread across modules or services. Parallel change is a refactoring technique that keeps old and new interfaces working side by side for a limited period. The migration happens in three stages: expand, migrate, and contract.

JavaScript 02 Sep 2026 6 min read

JavaScript Property Descriptors with Object.defineProperty

Most JavaScript properties are created with assignment or object literals. That is usually the right choice, but it hides several controls that every property carries: whether the property can be assigned, whether common enumeration APIs expose it, and whether its definition can later be changed. Property descriptors make those controls explicit. They are useful for library APIs, metadata, computed properties, compatibility layers, and cases where ordinary assignment exposes more behavior than intended.

Rust 01 Sep 2026 3 min read

Use the Typestate Pattern to Make Invalid Rust States Unrepresentable

Many APIs have lifecycle rules: a connection must be opened before sending, a transaction must begin before committing, or a builder must receive required values before producing output. Runtime flags can enforce these rules, but Rust can sometimes encode them in types instead. The typestate pattern represents each valid state with a distinct type and makes transitions consume one state to produce another. Encode states as marker types use std::marker::PhantomData; struct Disconnected; struct Connected; struct Connection<State> { endpoint: String, _state: PhantomData<State>, } impl Connection<Disconnected> { fn new(endpoint: String) -> Self { Self { endpoint, _state: PhantomData } } fn connect(self) -> Connection<Connected> { Connection { endpoint: self.endpoint, _state: PhantomData, } } } impl Connection<Connected> { fn send(&self, payload: &[u8]) { println!("sending {} bytes", payload.len()); } } send does not exist for Connection<Disconnected>. Incorrect call ordering becomes a compile-time error instead of a branch in production.

Python 01 Sep 2026 3 min read

Use Python Protocols for Structural Typing at API Boundaries

Python often relies on duck typing: if an object supports the operation a function needs, its concrete class does not matter. typing.Protocol gives static type checkers a way to describe that idea explicitly without requiring implementations to inherit from a shared base class. Define the behavior you consume from typing import Protocol class ByteWriter(Protocol): def write(self, data: bytes) -> int: ... def emit_header(writer: ByteWriter) -> None: writer.write(b"NLR1") Any statically compatible object can satisfy ByteWriter, even if its class never mentions the protocol.

Software Engineering 01 Sep 2026 5 min read

Evolving APIs Without Breaking Clients

An API is not only an HTTP path or function signature. It is a contract about syntax, semantics, timing, errors, ordering, defaults, and lifecycle. Breaking changes often happen because a server remains syntactically compatible while changing one of those less-visible assumptions. Safe API evolution starts by identifying what clients can reasonably depend on and designing changes that allow old and new versions to coexist. Compatibility has multiple dimensions A change can preserve JSON shape and still break clients.

Rust 01 Sep 2026 5 min read

Choosing &str, String, and Cow for Rust Text APIs

Rust has several common ways to represent UTF-8 text, and choosing between &str, String, and Cow<'a, str> is fundamentally an ownership decision. The best API is usually the one that asks callers for the least ownership it needs and returns ownership only when the result requires it. Use &str when you only need to read text A string slice borrows UTF-8 text owned elsewhere: fn is_blank(value: &str) -> bool { value.trim().is_empty() } This accepts borrowed views into a String, string literals, and other string slices without taking ownership or allocating.

Rust 01 Sep 2026 4 min read

Borrowed or Owned Data in Rust API Design

Rust APIs frequently face a design choice that is more important than syntax: should a function borrow data from the caller or take ownership of it? Borrowing can avoid allocation and make reuse cheap. Ownership can simplify storage and decouple lifetimes. Good APIs use each where it matches the actual data flow. Borrow when work is temporary If a function only reads a string during the call, accepting &str is usually natural: