Skip to content
All work

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

An API-first, event-driven integration topology: callers reach an API gateway, which publishes to an event bus; handlers process messages idempotently into systems of record, with bounded retry and a dead-letter route. retry exhausted bounded retry Callers apps · partners API gateway contract boundary Event bus queues · topics Handlers idempotent Systems of record Dead letter owner · runbook
  1. 1 Callers: internal platforms and partner systems, on both sides of the boundary.
  2. 2 API Management as the contract boundary — spec first, versioned, policy-enforced.
  3. 3 Service Bus and Event Grid: the default transport, so no caller waits on a third party.
  4. 4 Logic Apps and Functions doing the work, each idempotent on a business key.
  5. 5 Systems of record inside the enterprise boundary.
  6. 6 Dead-letter route with a named owner and a runbook. Bounded retry ends here, not in a loop.
The reference topology every interface is built against. Generic by necessity — no client system is named.

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

All work