Integration Mistakes That Break Teams, Projects, and APIs
The integration mistakes we see most often — from governance failures that sink projects before a line of code is written, to technical patterns that cause silent production failures. A practical guide with real examples, checklists, diagrams, and tooling.
Integration mistakes are the most expensive category of engineering failure: not because the problems are complex, but because they compound quietly. A governance failure at the start of a project creates pressure that surfaces six months later as a production outage. A missing timeout in a payment flow becomes a customer complaint before it becomes a ticket.
This guide covers both layers: the structural mistakes that kill integration projects before they ship, and the technical mistakes that break them in production. We've worked across enough integration projects: legacy modernisation, SaaS wiring, microservices decomposition: to see the same patterns repeat. Both layers matter. Both are predictable. Both are avoidable.
The two layers of integration failure
Most writing on integration mistakes picks a lane: either high level project failure (poor planning, unclear ownership, wrong tooling strategy) or low level technical failure (timeouts, retries, schema drift). Teams that struggle with integrations are usually dealing with both simultaneously: structural failures create the pressure that makes technical failures more likely, and technical failures surface the structural gaps that were always there.
Structural mistakes (the ones that kill projects)
Mistake 1: No clear ownership of the integration layer
Integration work sits between teams by definition: and "between teams" is where accountability disappears. When something breaks, each side assumes the other is responsible. When design decisions need to be made, no one has authority to make them. When the upstream changes a contract, nobody knows whose job it is to communicate it.
We've seen this failure mode repeat across organisations of every size. The specific form it takes varies: sometimes it's two product teams, sometimes it's an internal team and a third party vendor, sometimes it's a monolith being decomposed into services: but the root cause is always the same: integration ownership was assumed rather than assigned.
What to avoid "The platform team owns integrations" with no named individual accountable for any specific contract. Platform teams own the tooling. Someone else has to own each integration.
What this looks like when it fails: Two teams ship independently. One renames a field in their API response: a routine refactor, internal to their service. The other team's parsing breaks silently, writing malformed data for four days before a data quality alert fires. Both teams assumed the other was responsible for change communication. Neither had a documented contract. The postmortem identifies "communication failure" as the cause, which is accurate but not actionable.
The pattern that works: Assign a named owner for every integration contract: not a team, a person. That person is responsible for the contract definition, the versioning policy, the monitoring, and the incident response path. Document it. Put it in your service catalogue or your ADR. Make it boring and explicit.
Questions to answer before writing code:
- Who is the named owner of this integration contract?
- What is the process for communicating breaking changes?
- Who gets called when this integration fails at 2am?
Mistake 2: Treating integration as a point solution rather than a platform
The first integration gets solved with a direct HTTP call and a try/catch. The second one too. By the sixth, you have six different retry strategies, four different error handling conventions, three different timeout defaults, and no consistent observability across any of them. Every new integration is a custom solution to a problem that has already been solved five times, each time slightly differently.
What this costs at scale:
- Incident response is slower because each integration behaves differently under failure
- New team members have to learn each integration individually
- Bugs fixed in one place aren't fixed everywhere
- Observability is inconsistent, so you can't compare integration health across the system
What to avoid Copying the HTTP client setup from the last integration and tweaking it for the new one. Six months later, the retry logic has six slightly different implementations, two of which have bugs that were never discovered because nobody was looking at that integration's error rate.
The structural decision to make early:
Is integration a point solution or a platform? A platform approach means:
- Shared HTTP client with consistent timeout and retry defaults
- Consistent contract format (request/response shape, error types, correlation IDs)
- Centralised observability: every integration emits the same metrics
- Reusable adapter patterns for common upstream categories (REST, message queue, webhook)
The upfront investment is real: probably two to four weeks to build the foundation properly. The leverage shows up on integration three, pays back fully by integration five, and compounds from there.
When to think platform: If you expect more than three integrations, or if integrations are central to your product rather than incidental glue. If you're a team of two building a startup's first external API connection, a point solution is fine. If you're wiring together a microservices architecture or connecting a legacy system to a modern stack, the platform approach is not optional.
Mistake 3: Skipping the data ownership conversation
When two systems exchange data, someone has to own the schema. "We'll figure it out as we go" means the stronger team: usually the upstream: implicitly owns it, and the downstream builds brittle dependencies on undocumented behaviour. Implicit ownership is not ownership. It's a future incident waiting to be triggered.
What to avoid "The upstream team owns their API, so they own the schema." This is true in a narrow sense and dangerous in practice. The downstream team has no visibility into upcoming changes, no voice in versioning decisions, and no process for raising concerns before a breaking change ships.
What happens without this conversation: An upstream team deprecates a field in their internal roadmap. It's noted in their internal docs but not communicated externally. Six weeks later, the field stops being populated. Downstream systems that relied on it start returning empty results. No error: just silent degradation. The downstream team finds out when a customer reports incorrect data. Tracing the root cause takes days because there's no contract document to diff against.
The governance questions to answer before writing code:
| Question | Why it matters |
|---|---|
| Who owns the schema? | Determines who has authority to change it |
| How are breaking changes communicated? | Prevents silent degradation |
| What is the deprecation timeline? | Gives downstream teams time to adapt |
| Who resolves conflicts when systems disagree on state? | Prevents data quality incidents |
| What happens when upstream is unavailable? | Forces upstream to design for resilience |
These are governance questions, not technical ones. They feel slow and bureaucratic to answer upfront. They feel catastrophic to answer after the first major incident.
Technical mistakes (the ones that break production)
Mistake 4: Trusting the happy path
The first version of any integration assumes the other service is fast, online, and well behaved. Production disagrees: and it will disagree at the worst possible time.
Every outbound call needs three things before it goes near production:
- A timeout (if you don't set one, you're at the mercy of the upstream forever)
- A retry policy with exponential backoff (naive immediate retries amplify failures)
- A fallback decision: what happens when retries are exhausted? Fail loudly, queue for later, or serve degraded?
const res = await fetch(url, { signal: AbortSignal.timeout(5_000) });
if (!res.ok) {
// Don't swallow this. Decide: retry, queue, or fail loudly.
throw new IntegrationError(`Upstream returned ${res.status}`);
}
What this looks like at scale: A single integration point with no timeout caused a 40 minute partial outage for a client: one upstream started hanging on connections, the thread pool exhausted, and everything behind it queued to a halt. The fix was a one line timeout. The diagnosis took three hours.
Pattern to use: Circuit breaker + exponential backoff with jitter. Libraries like cockatiel (Node) or Polly (.NET) give you this out of the box.
Mistake 5: Retrying non idempotent operations
Retries are safe for GET. They're dangerous for "create payment", "send email", or "provision account". If you retry a request that already succeeded but whose response was lost in transit, you can double charge a customer, send duplicate notifications, or create ghost records.
The fix is idempotency keys: a unique ID per operation that the receiver uses to deduplicate.
await fetch('https://api.payments.io/charge', {
method: 'POST',
headers: {
'Idempotency-Key': operationId, // UUID tied to this specific charge attempt
'Content-Type': 'application/json',
},
body: JSON.stringify({ amount, currency, customer }),
});
If the request is retried with the same key, the receiver returns the original result rather than processing it again. Stripe, and most well designed payment APIs, support this natively. If your upstream doesn't, implement deduplication on your side.
Checklist before retrying any write operation:
- Is this operation idempotent by nature? (e.g., setting a value vs. incrementing it)
- Does the upstream support idempotency keys?
- If not, do you have a deduplication layer?
Mistake 6: Parsing instead of validating
TypeScript types do not exist at runtime. Treating an upstream JSON response as if your type annotations were guarantees is how undefined is not a function ends up in your logs at 2am: with no clear indication of which field was missing or which service sent malformed data.
Validate at the boundary. A schema check turns a vague runtime crash into a clear, attributable error.
import { z } from 'zod';
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
plan: z.enum(['free', 'pro', 'enterprise']),
});
const raw = await res.json();
const user = UserSchema.parse(raw); // Throws with a clear message if upstream changes shape
Why this matters: Upstream APIs change without warning. A field gets renamed, a type changes from string to integer, a nullable field stops being nullable. Without boundary validation, that change silently corrupts your data or crashes your service. With it, you get an immediate, attributable error you can act on.
Tools: Zod (TypeScript), Pydantic (Python), Joi, Yup, or JSON Schema validation.
Mistake 7: Leaking upstream failure modes
When a downstream dependency returns a 503, your service shouldn't blindly return a 503 to its own callers. That couples your API contract to an implementation detail your callers can't see and can't do anything about.
Translate failures into your own contract:
try {
const result = await callUpstream();
return result;
} catch (err) {
if (err instanceof UpstreamTimeoutError) {
throw new ServiceUnavailableError('Processing temporarily unavailable');
}
if (err instanceof UpstreamValidationError) {
throw new InternalError('Unexpected upstream response shape');
}
throw err;
}
The broader principle: Your service's failure modes are part of its public contract. If callers have to understand your dependency tree to handle your errors, your abstraction is leaking. Define your error types, map upstream failures to them, and own your contract.
Mistake 8: No visibility when it breaks
If you can't answer "which integration is slow right now?" from a dashboard, you will find out from a customer instead.
Every external call should log three things:
- Correlation ID: so you can trace a request across services
- Latency: so you know when "slow" starts
- Outcome: success, failure, timeout, or degraded
const start = Date.now();
try {
const result = await callUpstream(correlationId);
logger.info({ correlationId, latency: Date.now() - start, outcome: 'success', service: 'payments' });
return result;
} catch (err) {
logger.error({ correlationId, latency: Date.now() - start, outcome: 'failure', error: err.message, service: 'payments' });
throw err;
}
Tooling: OpenTelemetry is the standard for structured observability across services. Pair it with Grafana, Datadog, or a self hosted collector and you get distributed tracing, latency histograms, and error rates per integration: without waiting for a customer complaint to know something is wrong.
How these mistakes compound
These failures rarely appear in isolation. A common production failure chain:
- No ownership defined: so no one catches that the upstream changed its schema (Mistake 1 + 6)
- No platform conventions: so this integration's retry logic is unique and untested (Mistake 2)
- No data ownership conversation: so the schema change wasn't communicated (Mistake 3)
- No timeout on the external call: so the failure hangs rather than fails fast (Mistake 4)
- Retry fires immediately on failure, hitting a non idempotent endpoint (Mistake 5)
- The retried response has a slightly different shape, crashes unparsed (Mistake 6)
- The 500 leaks to the caller unchanged (Mistake 7)
- No correlation ID: diagnosis takes hours (Mistake 8)
Each mistake is fixable in isolation. Together, they turn a routine upstream hiccup into a multi hour incident with no clear owner and no audit trail.
Integration health checklist
Run this against any integration before it ships:
| Check | Layer | How to verify |
|---|---|---|
| Named owner assigned to this contract | Structural | Integration spec / service catalogue |
| Platform vs. point solution decision recorded | Structural | ADR exists |
| Data ownership and schema governance documented | Structural | Contract document exists |
| Breaking change process agreed with upstream | Structural | Documented in contract |
| Timeout set on every outbound call | Technical | Code review / grep |
| Retry policy with backoff documented | Technical | ADR or integration spec |
| Idempotency keys on all write operations | Technical | API contract review |
| Schema validation at the boundary | Technical | Test with malformed response |
| Upstream errors translated to own contract | Technical | Error handler coverage |
| Correlation ID propagated | Technical | End to end trace in staging |
| Latency and outcome logged | Technical | Verify in log output |
| Dashboard exists for this integration's health | Technical | Check monitoring setup |
FAQ
What is the single most common integration mistake? Missing timeouts on outbound calls. It's the simplest fix and the most frequently skipped. A call with no timeout is a latent outage waiting for an upstream to hang.
How do I know if my integration is healthy? You should be able to answer three questions from a dashboard without looking at logs: Is this integration up? Is it slow? Is the error rate elevated? If any of those require a log query, visibility is insufficient.
What is an idempotency key and why does it matter? An idempotency key is a unique identifier you attach to a state changing request. If the request is retried, the receiver uses the key to detect the duplicate and return the original result instead of processing it twice. Without this, retrying a failed payment can double charge a customer.
When should I treat integration as a platform rather than a point solution? If you expect more than three integrations, or if integrations are central to your product. A platform approach: shared retry logic, consistent contracts, centralised observability: pays back within the first year at that scale.
What tools should I use for integration observability?
OpenTelemetry for instrumentation, paired with Grafana, Datadog, or a self hosted collector. For Node.js resilience patterns, cockatiel. For .NET, Polly. For schema validation, Zod (TypeScript) or Pydantic (Python).
How should breaking changes to an integration contract be handled? Define a deprecation policy before you need it. A common approach: maintain the old field or endpoint for a minimum period (e.g., 90 days) after announcing deprecation, communicate via a documented channel (email, Slack, changelog), and require the downstream to confirm migration before removal. The key is that this process is agreed upfront, not negotiated mid incident.
What is the difference between an integration mistake and an API bug? An API bug is a defect in a single service. An integration mistake is a failure in the contract, assumptions, or operational setup between services. Integration mistakes are often harder to diagnose because neither side appears broken when examined in isolation.
Bottom line
The integrations that hold up aren't built by the most experienced teams. They're built by teams that made explicit decisions: about ownership, about contracts, about failure modes: before writing a line of code, and then applied consistent technical patterns throughout.
The governance layer and the technical layer aren't separate concerns. They're the same concern at different altitudes. Getting both right is what separates integrations that quietly do their job from integrations that quietly generate incidents.
If you're working through an integration project and want a technical review of your architecture or resilience patterns, Allbright Systems works with engineering teams on exactly this.