01 Integration platform Construction · Transport 2024 — present
One platform, two countries, 500,000 messages a day
Two clients, two countries, one architecture: an Australian construction, property and finance group and a UK passenger transport operator, both connecting internal enterprise platforms to third parties that were never designed to be depended on. My scope is the whole of it — requirement analysis, technical design authority, solution design and implementation.
- 50+
- interfaces owned end to end
- 500K+
- messages per day
- 3 wks → 1 wk
- new-interface delivery
01
Context
Both clients had the same shape of problem from opposite directions. Internal platforms — finance, asset, scheduling — needed to exchange data with third-party systems they did not control: suppliers, government services, partner operators, payment providers. Neither client had a consistent way of doing it, so every new interface was a fresh negotiation about protocol, contract, error handling and who got paged when it broke.
The volume was not the hard part. 500,000 messages a day is unremarkable for Azure Integration Services. The hard part was that a meaningful fraction of those messages cross a boundary into a system having a bad day, and the architecture has to have already decided what happens then.
02
Constraints
- Throughput
- 500K+ messages per day sustained, with daily batch peaks well above the mean.
- Partial failure is normal
- Third-party endpoints time out, return 500s, accept a message and lose it, or come back and ask for everything again. None of these is an exception case.
- Two jurisdictions
- Separate tenancies, separate data residency obligations, and no shared runtime between the two client estates.
- Delivery pressure
- New interfaces were being requested faster than a three-week build could supply them.
- Mixed-seniority team
- Four junior developers to mentor, so the design had to be teachable and reviewable, not merely correct.
03
Architecture
- 1 Callers: internal platforms and partner systems, on both sides of the boundary.
- 2 API Management as the contract boundary — spec first, versioned, policy-enforced.
- 3 Service Bus and Event Grid: the default transport, so no caller waits on a third party.
- 4 Logic Apps and Functions doing the work, each idempotent on a business key.
- 5 Systems of record inside the enterprise boundary.
- 6 Dead-letter route with a named owner and a runbook. Bounded retry ends here, not in a loop.
04
Decisions
What was considered, what was chosen, and what that choice cost. The last line is the one that matters.
Decision 01
The contract is written before the implementation
- Considered
- build first and document after · a shared canonical data model · OpenAPI spec-first with APIM as the boundary
- Chose
- Spec-first delivery, with API Management as the enforced contract boundary between internal platforms and everything outside.
- Because
- The contract is the only artefact both sides can agree on before either has built anything. It turns an integration dispute into a specification dispute, which is enormously cheaper to settle. A canonical model was the tempting alternative and the one I rejected hardest: it makes every participant wait on a modelling decision none of them own.
- Gave up
- Real time spent agreeing a spec before anything runs, and specs drift from implementations unless the drift is caught in CI.
Decision 02
Asynchronous by default, synchronous by exception
- Considered
- synchronous REST throughout · messaging throughout · per-use-case choice, decided case by case
- Chose
- Service Bus and Event Grid as the default transport, with a synchronous path only where the caller genuinely cannot proceed without the answer.
- Because
- A synchronous chain is only as available as its least available link, and one of those links is always a third party. Decoupling the caller from the callee's availability is the single highest-leverage reliability decision in this kind of estate. Deciding case by case sounds pragmatic but produces an estate where nobody can predict how anything behaves.
- Gave up
- End-to-end tracing gets materially harder. "Did it work?" stops being a response code and becomes a question you have to have instrumented for in advance.
Decision 03
Idempotency keyed on the message, not the delivery
- Considered
- a central deduplication table · broker-level exactly-once semantics · idempotent handlers keyed on a business identifier
- Chose
- Idempotent handlers, each keyed on a business identifier that is meaningful to the interface.
- Because
- At-least-once is the only honest assumption when a third party can replay, and exactly-once at the broker is a promise that dissolves the moment a handler has a side effect. The handler is the one place in the system that knows what "the same thing twice" actually means for its domain — a duplicate invoice and a duplicate telemetry reading are not the same problem.
- Gave up
- Every handler now carries the burden, and there is no central place to enforce it. It has to be a review checklist item, which means it depends on the review being done.
Decision 04
Templates, not a shared framework
- Considered
- an internal integration framework owned by the team · code generation from the spec · reusable Logic App, APIM policy and pipeline templates
- Chose
- Standardised templates that a team copies and adapts, rather than a framework they depend on.
- Because
- A shared framework becomes a product: it needs an owner, a backlog, a release cadence and a deprecation policy, and it couples two client estates that have every reason to diverge. Templates diverge too — but here divergence is a feature, because the clients genuinely differ.
- Gave up
- No single place to fix a systemic bug. A flaw in a template has to be chased across the interfaces that copied it.
05
Trade-offs accepted
- The estate is harder to observe than a synchronous one would be, and that cost is paid up front in instrumentation rather than later in incidents. It is the right trade only because the instrumentation actually got built.
- Standardising on templates accepted long-term drift in exchange for short-term delivery speed. Revisit that when the two estates stop diverging.
- Spec-first slows the first interface of any new integration pattern and speeds up every one after it. On a short engagement it would be the wrong call.
06
Stack
- Logic Apps
- API Management
- Azure Functions
- Service Bus
- Event Grid
- Bicep
- Azure DevOps
- Application Insights