Most failures don’t happen inside a component
They happen at the boundary.
A service can be correct in isolation and still cause outages, slowdowns, and missed deadlines.
Because integration is not “calling another thing.”
Integration is making a promise.
And promises have a lifespan.
A contract is a promise with consequences
A contract is anything another part of the system depends on:
- an API shape and its semantics
- an event schema and its meaning
- a data format, a topic name, an error code
- even the order in which steps happen
Once someone depends on it, it stops being “your implementation detail.” It becomes shared reality.
That’s why contract design matters more than most people expect.
The simplest rule: keep contracts smaller than your internals
Internals can change often. Contracts should not.
If you expose your internal model directly, you freeze it.
Then every refactor turns into a migration project. And every migration becomes a negotiation.
So the goal is simple:
Expose a stable surface. Keep the messy truth behind it.
Two kinds of integration, two kinds of risk
Synchronous APIs Fast feedback. Simple mental model. Also creates runtime coupling: latency stacks, failures propagate, retries amplify.
Asynchronous events Decouples runtimes. Buffers load. Supports multiple consumers. Also creates contract sprawl: schema evolution, consumer expectations, replay complexity.
Neither is “better.” Each has a failure mode. Your job is to choose the one you can manage.
The hidden integration: shared databases
If two components share tables, you created a contract. You just did it without naming it.
That contract is usually:
- undocumented
- brittle
- impossible to version safely
- easy to break by accident
A shared database can be useful early, but it is not “free.” It is one of the strongest forms of coupling you can create.
If you allow it, do it with intent. And treat it as a temporary strategy, not an architecture style.
Design contracts for change, not for today
Most teams design contracts for the first release.
Then reality arrives:
- new clients
- new edge cases
- new compliance rules
- new consumers you didn’t predict
The contract survives only if it can evolve.
A practical default:
Backward-compatible changes should be the normal path.
Breaking changes should be rare, staged, and explicit.
A clean way to evolve an API
When you must change behavior, avoid the “big switch.”
Use a staged approach:
- Add the new thing (new field, new endpoint, new option)
- Support both paths for a while
- Deprecate the old path with clear timelines
- Remove only when usage is near zero
This looks slower at first. It is faster than an emergency rollback.
Also: document semantics, not just shapes. “200 OK” is not a contract. Meaning is.
A clean way to evolve events
Events are powerful because they are a history of facts.
But that also means they outlive you.
If you publish events, treat them like products:
- they have owners
- they have compatibility rules
- they have documentation
- they have consumers you may not know about
Two practical rules:
1) Publish business facts, not internal steps. “OrderPlaced” ages better than “OrderServiceStartedTransaction.”
2) Prefer additive changes. Adding fields is safer than renaming or changing meaning.
Once an event becomes widely used, breaking it is expensive. Because you don’t control all consumers.
Translation is not overhead. It’s insulation.
External systems, vendors, legacy APIs — they come with their own models.
If you let those models leak into your core, your system becomes a patchwork of someone else’s assumptions.
So you translate at the boundary:
- map external terms to your terms
- keep your domain language consistent
- prevent “foreign concepts” from spreading
This makes future change cheaper:
- switching vendors
- modernizing legacy parts
- supporting new channels
Clean translation protects your system’s meaning.
Contracts need tests, not just documentation
Documentation helps people.
Tests protect systems.
If a contract matters, you need checks that run every time:
- contract tests for APIs
- schema compatibility checks for events
- smoke tests for critical workflows
- alerts when error rates or latency break your assumptions
This is how you stop drift.
Because pressure will always push teams to “just ship.” Guardrails are what keep shipping safe.
Closing
Integration is where architecture becomes real.
Inside a component, you can refactor. At the edges, you negotiate with time.
Keep contracts small. Design them for change. Translate at boundaries. And protect them with checks.
That’s how systems stay stable while everything else moves.
Key takeaways / refresher bullets
- Integration is a contract: a promise others depend on.
- Keep contracts smaller and more stable than your internal model.
- APIs and events have different risks: runtime coupling vs contract sprawl.
- Shared databases are contracts too — usually the most brittle ones.
- Default to backward-compatible evolution; stage breaking change.
- For events: publish business facts, prefer additive changes, treat events like products.
- Translate at boundaries to protect your core language and rules.
- Important contracts need automated checks or they will drift under pressure.