API Design: Write Around Changes, Not Reads
- Average speed
- km/h
- Result
- Finish time
- 50 min
- You entered
- Distance
- 10 km
- You entered
Designing an API around what changes, rather than what you read, means you build your interface to handle updates, new fields, and evolving client needs gracefully, instead of locking it to a fixed set of read-only endpoints. This approach prioritizes flexibility and forward compatibility, ensuring that as your data or business logic evolves, your API doesn’t break existing clients. In this article, we’ll explore why this principle matters, how to implement it, and the trade-offs involved.
Background: The Problem with Read-Centric APIs
Traditional APIs often expose a fixed set of resources and fields, designed around what the frontend currently displays. For example, a user profile API might return name, email, and age. But what happens when you need to add phone or preferences? You either create a new endpoint, version the API, or force clients to ignore extra fields. This leads to tight coupling between the API and the UI, making changes risky and slow. Read-centric designs focus on the present state, ignoring that data is dynamic and requirements shift.
How It Works: Designing for Change
Writing around changes means you think in terms of events, commands, and projections. Instead of exposing a static snapshot, you design an API that:
- Accepts commands (e.g.,
POST /orderswithplaceOrder) rather than generic CRUD that directly maps to database tables. - Returns projections that are derived from underlying data, so you can add or remove fields without breaking clients.
- Uses versioning and additive changes – adding new optional fields is backward-compatible, while removing or renaming fields is breaking.
- Embraces event sourcing or webhooks to notify clients of changes, rather than making them poll for updates.
This approach is often called “API-first� or “change-driven design.� It separates the “what� (the data) from the “how� (the read model).
How to Choose: When to Use This Approach
Not every API needs this complexity. Consider the following:
- Use change-driven design when your data is highly dynamic, you expect frequent feature additions, or you have multiple clients (web, mobile, third-party) that need different views.
- Stick with read-centric for simple, stable data like a public reference list (e.g., country codes) where changes are rare.
- Hybrid approach – have a core command API and separate read models (e.g., using CQRS – Command Query Responsibility Segregation).
Ask yourself: How often will this data change? Who consumes it? Can I afford versioning? If the answer is “often� and “many consumers,� then write around changes.
Common Problems and Misconceptions
One common misconception is that “write around changes� means you must use event sourcing or microservices. That’s not true – it’s a design philosophy, not a technology stack. Another problem is over-engineering: some teams build overly generic APIs that accept any JSON blob, leading to validation nightmares. Also, developers often forget that reads are also changes – if you add a field, you might need to update the read model. Misunderstanding the difference between a command (intent) and a query (read) leads to muddled designs.
Comparison: Command vs. Query APIs
Let’s compare the two styles:
- Command API (write-focused):
POST /orderswith{ "productId": 123, "qty": 2 }– the server decides what to do. It returns a status or a resource ID. It’s action-oriented. - Query API (read-focused):
GET /orders/123returns the full order details. It’s data-oriented.
In a change-driven design, you often separate these: commands are the primary interface, and queries are built on projections. This allows you to change the underlying storage or logic without affecting how clients issue commands.
Costs and Trade-offs
Change-driven APIs have higher upfront design costs. You need to model commands, define event schemas, and build projection systems. This requires more planning and often more infrastructure (e.g., event stores, message brokers). The payoff is long-term agility: you can evolve your API with less breaking changes. However, for small projects or prototypes, the overhead may not be worth it. Also, debugging can be harder because you have asynchronous flows and eventual consistency.
Safety and Best Practices
When designing for change, follow these safety guidelines:
- Never remove a field – deprecate it and keep it for a while.
- Use semantic versioning for your API (e.g., /v1/, /v2/).
- Validate all inputs – even if you accept flexible commands, ensure they meet invariants.
- Provide clear error messages – when a command fails, tell the client why.
- Document changes – changelogs and migration guides are essential.
Frequently Asked Questions
Q: Does “write around changes� mean I should use event sourcing?
No, it’s a design principle. Event sourcing is one way to implement it, but you can also use simple command handlers with a relational database.
Q: How do I handle reads then?
You can create dedicated read endpoints that return projections. These can be cached or denormalized for performance. They don’t have to be real-time.
Q: What if I have a legacy API that’s read-centric?
You can gradually introduce command endpoints and keep the old ones for compatibility. Over time, you can migrate clients to the new style.
Q: Is this approach only for large systems?
No, but it’s most beneficial when you expect growth. For small apps, a simple REST API might suffice.
Q: How do I version a change-driven API?
Version the command contract, not the read model. If you change a command’s semantics, bump the version. Additive changes (new optional fields) don’t require a new version.
Q: What’s the biggest mistake in this design?
Making everything generic – e.g., accepting arbitrary JSON without a schema. That leads to unmaintainable code. Always define clear contracts.
Related
Formula used
in t | Finish time | min | 50 | 0 in d | Distance | km | 10 | 0 out pace | Average pace | min/km | t / d | 2 out speed | Average speed | km/h | d / (t / 60) | 2
Each line is one input (in, sel, list) or one result (out: label, unit, formula, decimals). How the calculators work
Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.