When Our APIs Became the Architecture

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.

Leave a Reply

Your email address will not be published. Required fields are marked *