At first, the API was just an interface.
A way for one component to call another.
Nothing more.
Then the system grew.
The monolith became multiple services.
Teams became independent.
Deployments became more frequent.
And suddenly almost every important business capability depended on an API.
Payment called Customer.
Order called Payment.
Settlement called Ledger.
Reporting consumed events and APIs.
Mobile applications depended on public endpoints.
Partner integrations depended on external contracts.
The architecture increasingly looked like this:
Customer
│
▼
Order API
│
▼
Payment API
│
▼
Ledger API
│
▼
Settlement
At that point, something became obvious:
The APIs were no longer just interfaces between components. They were the boundaries holding the architecture together.
And every change to an API became an architectural decision.
1. The First API Was Easy
The original API was simple.
For example:
POST /payments
Request:
{
"customerId": "12345",
"amount": 1000,
"currency": "USD"
}
Response:
{
"paymentId": "pay-123",
"status": "SUCCESS"
}
The producer knew the consumers.
The consumers knew the producer.
Changes could be coordinated.
If we needed to change the response, we could update everything together.
That works well when there is one team.
It becomes much harder when ten teams depend on the same API.
2. Then the Consumers Multiplied
The Payment API eventually had consumers we didn’t control directly.
Payment API
│
┌─────────────┼─────────────┐
▼ ▼ ▼
Order Service Mobile App Partner API
│ │ │
▼ ▼ ▼
Settlement Web Client External System
Now changing the API meant potentially changing multiple systems.
And those systems didn’t necessarily deploy at the same time.
One consumer might upgrade tomorrow.
Another might upgrade next month.
An external partner might not upgrade for six months.
The API had become a long-lived contract.
3. The Breaking Change
The first painful example was a seemingly harmless response change.
Originally:
{
"paymentId": "pay-123",
"status": "SUCCESS"
}
We wanted to make the response more descriptive:
{
"paymentId": "pay-123",
"status": {
"code": "SUCCESS",
"description": "Payment completed"
}
}
From our perspective, it was an improvement.
From the consumer’s perspective:
status == "SUCCESS"
was suddenly broken.
The API was technically returning valid JSON.
The application was still responding.
But we had changed the contract.
The problem wasn’t the code.
The problem was that we had treated an API as an implementation detail.
It wasn’t.
It was a dependency.
4. Backward Compatibility Became a Design Requirement
We started asking a different question before changing an API:
Can existing consumers continue working without modification?
That became one of our most important compatibility principles.
For example, adding an optional field is generally safer:
{
"paymentId": "pay-123",
"status": "SUCCESS",
"settlementDate": "2026-08-15"
}
Existing consumers can ignore the new field.
Changing an existing field’s meaning is much more dangerous.
So is:
- Removing a field
- Renaming a field
- Changing a field type
- Changing required/optional behavior
- Changing enum meanings
- Changing error semantics
- Changing authentication requirements
The syntax can remain valid while the contract becomes incompatible.
5. API Compatibility Is More Than JSON
We initially focused heavily on schemas.
But APIs have more than schemas.
An API contract can include:
Request format
Response format
HTTP status codes
Error structure
Authentication
Authorization
Idempotency behavior
Timeout expectations
Rate limits
Pagination
Ordering
Retry semantics
Consistency guarantees
For example:
POST /payments
might return:
201 Created
for a new payment.
A consumer may depend on that.
If we later change it to:
202 Accepted
because processing became asynchronous, we’ve changed the semantics of the API even if the JSON response looks identical.
The API contract includes behavior.
Not just fields.
6. Versioning Became Necessary
Eventually, some changes simply couldn’t be made backward-compatible.
That’s when versioning became useful.
For example:
/api/v1/payments
/api/v2/payments
The important principle wasn’t:
“Every API needs a new version for every change.”
That creates unnecessary complexity.
The principle was:
Version when the contract meaningfully changes and existing consumers cannot safely continue using the old behavior.
A compatible change might not require a new version.
A breaking change might.
7. Versioning Doesn’t Solve Everything
Versioning sounds easy:
v1
v2
v3
v4
But eventually you have to support all of them.
Payment API
/ | \
v1 v2 v3
Now the engineering team has to maintain:
- Multiple schemas
- Multiple behaviors
- Multiple test suites
- Migration paths
- Documentation
- Monitoring
- Deprecation policies
Versioning can protect consumers.
But uncontrolled versioning can create a permanent compatibility burden.
So we learned:
Versioning is a migration strategy, not a substitute for API design.
8. The Schema Evolution Problem
Schema evolution became another major concern.
Suppose the original request is:
{
"amount": 1000
}
We later require:
{
"amount": 1000,
"currency": "USD"
}
If currency becomes mandatory immediately, old consumers break.
A safer evolution might be:
Step 1
Accept requests with or without currency
Step 2
Update all consumers to send currency
Step 3
Monitor adoption
Step 4
Make currency mandatory
Step 5
Remove legacy behavior
This is similar to the expand-and-contract approach we used for database migrations.
The API itself needs an evolution strategy.
9. Contract Testing Changed How We Worked
One of the biggest improvements was introducing contract testing.
Instead of only testing the provider:
Does Payment API work?
we also tested:
Does Payment API still satisfy what its consumers expect?
The consumer defines important expectations.
For example:
Consumer expects:
POST /payments
Response:
paymentId = string
status = SUCCESS | FAILED | PENDING
The provider’s build can verify that it still satisfies those expectations.
This gives us earlier feedback.
A provider change that would break a consumer can be detected before production.
10. Consumer-Driven Contracts
This became particularly valuable when teams were independently deployed.
Imagine:
Order Service
│
│ depends on
▼
Payment Service
The Payment team might not know every assumption made by Order.
The Order team does.
A consumer-driven contract makes those expectations explicit.
For example:
Order Service requires:
POST /payments
status must exist
paymentId must exist
FAILED must include an error code
The provider can validate that contract continuously.
This creates a much stronger relationship between independently deployed services.
11. The API Became a Team Boundary
This was one of the more surprising lessons.
An API didn’t just separate software components.
It separated teams.
For example:
Payment Team
│
│ API contract
▼
Order Team
The Payment team could change its internal implementation without involving Order.
But if it changed the contract, coordination was required.
The API therefore became part of the organizational architecture.
This connected directly to what we learned about Conway’s Law.
Team boundaries influence system boundaries.
And APIs are often where those boundaries become visible.
12. Internal APIs Can Become Permanent APIs
We initially treated internal APIs differently from public APIs.
That was a mistake.
An API doesn’t become unimportant just because it is internal.
If ten internal services depend on:
/payment-service/v1/payments
then that interface has become a significant architectural contract.
It deserves:
- Documentation
- Compatibility rules
- Testing
- Ownership
- Deprecation policies
- Observability
Internal doesn’t mean temporary.
13. API Contracts Include Failure Behavior
This became especially important in financial systems.
Suppose:
POST /payments
times out.
What does that mean?
Did the payment fail?
Did the payment succeed?
Is the result unknown?
The API contract needs to define this.
For example:
200 → payment completed
202 → payment accepted for processing
400 → invalid request
409 → duplicate/idempotency conflict
500 → processing failure
But even this isn’t enough.
A timeout doesn’t necessarily mean the payment didn’t happen.
The consumer needs a safe recovery strategy.
This is why APIs need explicit semantics around:
- Idempotency
- Retries
- Timeouts
- Unknown outcomes
- Status lookup
- Error classification
The contract extends into failure behavior.
14. Idempotency Became Part of the API
Consider:
POST /payments
A client sends the request.
The server processes the payment.
The network connection fails before the response reaches the client.
The client retries.
Without idempotency:
Request 1 → Charge
Request 2 → Charge
Potentially:
Two charges.
With an idempotency key:
Idempotency-Key: payment-847291
the API can recognize that the operation has already been processed.
The API contract now includes not only:
“What fields do I send?”
but:
“What happens if I send the same request twice?”
That’s an architectural contract.
15. We Had to Define Error Semantics
Another problem was inconsistent error handling.
One service returned:
{
"error": "INVALID_PAYMENT"
}
Another returned:
{
"code": "PAYMENT_INVALID"
}
Another returned:
{
"message": "Payment failed"
}
Consumers had to understand every variation.
We eventually standardized important error semantics.
For example:
{
"code": "PAYMENT_DECLINED",
"message": "Payment could not be completed",
"retryable": false,
"correlationId": "abc123"
}
The exact format isn’t universal.
The important thing is that consumers can reliably distinguish:
Retryable
Non-retryable
Validation failure
Authentication failure
Authorization failure
Conflict
Unknown outcome
This prevents dangerous retry behavior.
16. API Evolution Became a Migration Process
We stopped thinking about API changes as:
Developer changes code
↓
Deploy
Instead:
Design change
↓
Compatibility analysis
↓
Contract update
↓
Consumer migration
↓
Observability
↓
Staged rollout
↓
Deprecation
↓
Removal
This was slower.
But much safer.
Especially when the API controlled financial operations.
17. We Used Additive Changes Where Possible
One of our preferred strategies became:
Add before removing.
Suppose we wanted to replace:
{
"customerName": "John Smith"
}
with:
{
"customer": {
"name": "John Smith"
}
}
Instead of immediately removing the old field:
Phase 1
Add customer.name
Phase 2
Update consumers
Phase 3
Monitor old-field usage
Phase 4
Deprecate customerName
Phase 5
Remove customerName
This gave consumers time to migrate.
The same principle works for many distributed-system contracts.
18. Observability Became Part of API Governance
We couldn’t safely deprecate an API if we didn’t know who was still using it.
So we started tracking:
Requests by API version
Requests by consumer
Error rates
Latency
Deprecated field usage
Deprecated endpoint usage
For example:
/v1/payments
Consumer A → 0%
Consumer B → 2%
Consumer C → 0%
Consumer D → 41%
Now we knew who still needed migration.
Without usage visibility, API deprecation becomes guesswork.
19. APIs and Events Have Different Contracts
As the system became more event-driven, we also had to distinguish APIs from events.
An API is often:
Request
↓
Response
An event is:
Something happened
↓
Consumers react
For example:
POST /payments
is an operation.
Whereas:
PaymentCompleted
is a fact.
They have different evolution requirements.
But both are contracts.
An event schema can be just as difficult to change as an API schema when hundreds of consumers depend on it.
20. The Same Principle Applied to Events
Suppose an event originally contains:
{
"paymentId": "pay-123",
"status": "COMPLETED"
}
We later add:
{
"paymentId": "pay-123",
"status": "COMPLETED",
"settlementReference": "set-456"
}
Existing consumers can usually ignore the new field.
But changing:
status
from a string to a nested object could break consumers.
The lesson is the same:
Once other systems depend on your contract, evolution must be deliberate.
21. What We Actually Changed
Our API architecture eventually became more disciplined.
Every important API had:
An owner
Someone was responsible for the contract.
A documented contract
Consumers knew what behavior to expect.
Compatibility rules
Teams knew what changes were safe.
Contract tests
Breaking changes were detected early.
Versioning strategy
Breaking changes had migration paths.
Deprecation process
Old contracts weren’t removed without understanding usage.
Observability
We could see who depended on which version.
Failure semantics
Retries, errors, timeouts, and idempotency were explicit.
The API became a managed architectural boundary.
22. What We Would Not Do Now
We wouldn’t change an API because:
“It’s only an internal API.”
We wouldn’t remove fields simply because the producer no longer needs them.
We wouldn’t introduce a new API version for every minor change.
We wouldn’t assume schema compatibility means behavioral compatibility.
We wouldn’t rely only on provider-side tests.
We wouldn’t make breaking changes without understanding consumer usage.
We wouldn’t treat error handling as an implementation detail.
And we wouldn’t design payment APIs without thinking about retries and duplicate requests.
23. The Questions I Ask Before Changing an API
Before modifying an important API, I ask:
Who consumes this contract?
Then:
What assumptions do those consumers make?
Then:
Is the change backward-compatible?
Then:
Can we make the change additive?
Then:
Do we need a new version?
Then:
Can contract tests prove that existing consumers remain safe?
And finally:
How will we know when it is safe to remove the old contract?
Those questions prevent many production incidents.
24. The Bigger Architectural Lesson
We initially thought architecture was defined by:
Services
Databases
Queues
Infrastructure
But as the system matured, another layer became equally important:
Contracts
APIs define how services interact.
Events define how systems communicate facts.
Schemas define how data is interpreted.
Error semantics define how failures are handled.
Idempotency defines how retries behave.
Versioning defines how systems evolve.
Together, these contracts become the connective tissue of the architecture.
25. Final Thought
The most dangerous API changes aren’t necessarily the ones that cause immediate errors.
Sometimes the API continues returning 200.
The JSON is valid.
The service is healthy.
But the meaning has changed.
A consumer interprets the response differently.
A retry behaves differently.
A financial workflow enters a different state.
A downstream service makes the wrong decision.
That’s when we learned the real lesson:
An API is not just an interface. It is a long-term architectural contract.
Once other systems depend on it, changing it is no longer just refactoring.
It is architecture evolution.
And in financial systems, where APIs can trigger payments, settlements, ledger changes, and other irreversible operations, the contract must define not only what the system does when everything works, but what every consumer should expect when things don’t.
The best APIs aren’t the ones that never change.
They’re the ones that can evolve without breaking the systems that depend on them.