REF / WRITING · SOFTWARE

ESB + Event-Driven Architecture: Internal Services and Partner Integration

How an Enterprise Service Bus paired with event-driven architecture solves integration sprawl - internal service wiring and partner onboarding patterns.

DomainSoftware
Formattutorial
Published22 May 2026
Tagsarchitecture · esb · event-driven

Past a certain scale, the dominant operational cost in any enterprise system isn't building new services. It's connecting the ones you already have. Every new internal service needs to talk to half of the existing ones. Every new external partner is a bespoke integration project that takes a quarter to ship and ages badly. Schema changes ripple unpredictably across the codebase. Observability collapses because no single team can see the whole flow. And every few months, an outage somewhere in the integration mesh takes down something that nobody on call understands.

The architecture that solves this — at the scale where it actually matters — is an Enterprise Service Bus (ESB) paired with a full event-driven backbone. The two together do what neither does alone: the ESB collapses point-to-point integration sprawl into a single mediated layer, and the event bus turns the system from a network of connected services into a reactive one where producers and consumers don't know about each other.

This is the practitioner's guide to building it — what each layer does, how internal services wire into it, how external partners plug in without touching anything internal, what problems it actually solves, and the cases where it's the wrong choice.

The Problem: Integration Sprawl

Before the architecture makes sense, the problem it solves has to be visible. Picture a typical enterprise system that grew up organically — eight services, each integrating directly with the four or five others it needs to talk to:

   [Service A] ------- [Service B] ------- [Service C]
       |  \           /     |   \           /  |
       |   \         /      |    \         /   |
       |    \       /       |     \       /    |
   [Service D] -- [Service E] -- [Service F]
       |     /        |        \     |
       |    /         |         \    |
   [Service G] ----------- [Service H]

Every line is a contract that has to be maintained. Every contract has versioning concerns, authentication, retry semantics, observability instrumentation. The math is unforgiving: with N services, you can have up to N(N-1)/2 connections. At eight services, that's 28. At twenty, it's 190. At fifty, it's over a thousand.

Beyond the raw count, three failure modes compound:

  • Schema changes ripple unpredictably. A change in Service A's response shape has to be co-ordinated with every service that consumes it — which often nobody has fully mapped.
  • Failure modes consolidate in unfortunate places. When Service E goes down, you discover at 2am that seven other services depend on it in ways nobody documented.
  • Observability collapses. No single team owns the integration mesh, so when something is slow, the answer is "somewhere in the middle of all these connections, but nobody is sure where."

This is the state most growing enterprises arrive at well before they decide to address it. The architecture below is what fixes it.

What the ESB Brings to the Table

The ESB is a central mediation layer that handles routing, transformation, protocol translation, and security between services. Each service only needs to know how to talk to the bus; the bus handles everything else.

       +-----------+      +-----------+      +-----------+
       | Service A |      | Service B |      | Service C |
       +-----+-----+      +-----+-----+      +-----+-----+
             |                  |                  |
             |                  |                  |
       +-----+------------------+------------------+-----+
       |                                                  |
       |              ENTERPRISE SERVICE BUS              |
       |     routing | transformation | mediation         |
       |                                                  |
       +-----+------------------+------------------+-----+
             |                  |                  |
       +-----+-----+      +-----+-----+      +-----+-----+
       | Service D |      | Service E |      | Service F |
       +-----------+      +-----------+      +-----------+

The shape of the connection diagram is the point. Where the point-to-point system had up to N² connections, the ESB topology has N — one per service. Adding a new service adds one connection, not N. The mediation layer absorbs the cross-cutting concerns:

  • Protocol translation. Service A sends XML over SOAP; Service D needs JSON over REST. The bus handles the translation. Neither service knows the other's protocol exists.
  • Format transformation. Service B emits customer.id, but Service E expects customerNumber. The mapping lives in the bus, not in either service.
  • Centralised routing rules. "Requests for accounts opened before 2024 go to the legacy service; everything else goes to the new one." That logic lives in one place.
  • Cross-cutting security. Authentication, authorisation, encryption, audit logging — all enforced at the bus, applied uniformly to every service.

The ESB's natural mode is synchronous request/response. Service A calls the bus; the bus routes the call to Service D, transforms the response on the way back. This is the right pattern when the caller needs the answer to proceed — "give me this customer's order history right now so I can show it on the screen."

For the other half of the integration picture — reactive flows where producers don't know or care who's listening — the synchronous bus isn't the right tool. That's what event-driven adds.

What Event-Driven Adds

The asynchronous half of the architecture. Services publish events to named topics; subscribers react independently of the producer.

   [Order Service]
         |
         | publishes
         v
   +--------------------+
   |  Topic:            |
   |  order.created     |
   +---------+----------+
             |
       +-----+-----+-----+-----+
       |     |     |     |     |
       v     v     v     v     v
   [Inv] [Email] [Anal] [Audit] [Loyalty]

The Order Service publishes one order.created event. Five subscribers react: inventory decrements stock, email sends a confirmation, analytics records the conversion, audit logs the transaction for compliance, the loyalty service updates points. The Order Service has no idea who's subscribed. Adding a sixth subscriber — say, a fraud-detection pipeline — requires zero changes to the Order Service.

Three properties of this pattern make it transformative:

  • Producer decoupling. Producers don't know who consumes their events. Consumers can be added, removed, or refactored without touching the producer.
  • Fan-out is free. One event triggers many independent reactions. The shape of "one thing happened, several other things should happen as a result" is now native.
  • Eventual consistency by default. Subscribers process events at their own pace. Slow subscribers don't slow down the producer. Failed subscribers retry independently without blocking the rest of the chain.

The shift in mental model is the point. A point-to-point system is connected. An event-driven system is reactive. The difference is that in a reactive system, the producer's responsibility ends at "publish what happened." Everything downstream becomes someone else's problem in the best sense — independently observable, independently deployable, independently testable.

The Combined Architecture

The synthesis is more than the sum of its parts. The ESB handles synchronous request/response and orchestration. The event bus handles asynchronous reaction and fan-out. They share a service registry, a unified security model, and a single observability layer.

   +---------------+    +---------------+    +---------------+
   |   Service A   |    |   Service B   |    |   Service C   |
   +-------+-------+    +-------+-------+    +-------+-------+
           |                    |                    |
   +-------+--------------------+--------------------+-------+
   |                                                         |
   |              ENTERPRISE SERVICE BUS  (sync)             |
   |                                                         |
   +-------+--------------------+--------------------+-------+
           |                    |                    |
           |   publishes/subscribes via shared topics
           |                    |                    |
   +-------+--------------------+--------------------+-------+
   |                                                         |
   |               EVENT BUS  (async, pub/sub)               |
   |                                                         |
   +-------+--------------------+--------------------+-------+
           |                    |                    |
   +-------+-------+    +-------+-------+    +-------+-------+
   |   Service D   |    |   Service E   |    |   Service F   |
   +---------------+    +---------------+    +---------------+

The decision rule is straightforward. Synchronous ESB for "give me data now" — anything where the caller needs the answer to proceed, where the call is a question rather than a notification. Asynchronous event bus for "tell everyone this happened" — anything where the producer's job ends at publishing the fact, and the system is supposed to react in parallel.

In practice, most services use both. A typical order service might:

  • Make a synchronous ESB call to the inventory service to check stock before accepting the order (caller needs the answer).
  • Make a synchronous ESB call to the payment service to authorise the card (caller needs the answer).
  • Publish an event to order.created once the order is accepted (producer's job ends; consumers react asynchronously).

The architectural discipline is keeping the two modes clearly separated. Mixing them — making synchronous calls when an event would do, or fanning out asynchronously when the caller actually needs an answer — produces systems that are hard to reason about and worse to debug. The pattern is the same one I've written about for boring-architecture-over-clever-architecture in AI agent design — the failures come from blurring the boundary between sync and async, not from one or the other being insufficient.

Wiring Up Internal Services

How a new internal service joins the architecture, step by step.

1. Register the service in the service catalog. Declare its name, owner team, contract, version, and SLA. The catalog is the single source of truth for "what services exist and how to talk to them." Every other step references entries here.

2. Define routing rules in the ESB. For each endpoint the service exposes, declare: input format, validation rules, transformations needed, target endpoint (the service's own URL behind the bus), retry policy, circuit breaker thresholds. This is the configuration that turns "service registered" into "service reachable."

3. Declare which topics the service publishes to. Each domain event the service emits — order.created, inventory.depleted, customer.upgraded — needs an explicit schema in the schema registry. Producers declare what they publish; subscribers know what shape to expect. Schema evolution is governed by the registry.

4. Subscribe to the topics it reacts to. Mirror image of step 3. The service declares which event topics it consumes and the handler that processes them. The event bus delivers events with retry, dead-letter, and ordering guarantees per the topic's configuration.

5. Wire up authentication via the central identity service. The service identifies itself to the bus with a credential issued by the central identity system. The bus enforces authorisation rules: which services can call which endpoints, which topics they can publish to, which they can subscribe to. No service handles its own auth.

6. Instrument metrics and tracing via the shared observability stack. Every call through the bus and every event published gets a trace ID that propagates through subsequent calls and events. The observability layer can reconstruct the full causal chain of "this user clicked this button → eleven services were called or notified → here's where the slow part was."

Each of these is a configuration step, not a code-change project. Adding a service is roughly an afternoon's work once the architecture is in place. The cost of every subsequent integration drops dramatically — which is the whole point. If you're at the scale where this complexity isn't justified yet, the Spring Boot modular monolith piece covers the right architecture for the earlier stage.

Onboarding External Partners

External partners — vendors, customers integrating against your APIs, regulators, fulfilment partners — never touch internal services directly. They terminate at an edge layer that mediates everything.

   [Partner A]   [Partner B]   [Partner C]
       |             |             |
       v             v             v
   +-------------------------------------+
   |        API GATEWAY / EDGE           |
   |  auth | rate-limit | validation     |
   +------------------+------------------+
                      |
                      v
   +-------------------------------------+
   |     PARTNER ADAPTER LAYER           |
   |  format translation | normalisation |
   +------------------+------------------+
                      |
       +--------------+---------------+
       v                              v
   +-------+                     +---------+
   |  ESB  |  <---------------> | EVENTS  |
   +-------+                     +---------+

Two distinct layers between the partner and the internal architecture.

The API gateway / edge layer. Handles everything that doesn't depend on what the partner is asking for: authentication (API keys, OAuth tokens, mTLS certificates), rate limiting (per-partner quotas), contract validation (schema-check the incoming request), and quota enforcement. This is the security perimeter. Nothing past this layer needs to handle "is this partner allowed to do this?" — the gateway already answered.

The partner adapter layer. Translates the partner's specific format into the internal canonical format. Different partners send wildly different shapes — one sends EDI, another sends SOAP-wrapped XML, a third sends a partner-specific JSON dialect. The adapter layer absorbs all of this. Internally, every request from every partner arrives at the ESB in the same canonical shape.

This separation gives you three important properties:

  • Partner isolation. Internal services never see partner-specific formats. A breaking change on the partner side requires updating the adapter, not the internal service.
  • Onboarding is configuration. Adding a new partner becomes "configure the gateway for their auth, write the adapter for their format" — typically a week, not a quarter.
  • Compliance and audit trail. All partner traffic flows through a known set of mediation layers. Regulatory questions about who called what when have a clean, single-source answer.

The internal-to-internal call patterns from Go gRPC microservices in production still apply behind the partner layer — they just operate on the canonical internal format, not on whatever the partner happened to send.

What This Architecture Solves

The pros worth being explicit about. After watching teams adopt this pattern across several engagements, the concrete problems it addresses:

  • Integration sprawl collapses from N² to N. New services add a single connection (to the bus), not N. The marginal cost of adding the tenth service is the same as the fiftieth.
  • Schema evolution becomes manageable. Format changes are absorbed in the ESB and the adapter layer. Internal services see a stable canonical format regardless of what producers or partners do.
  • Failure modes consolidate into observable places. When something goes wrong, it goes wrong in the bus, the gateway, the adapter, or a known service — not in an undocumented connection between two services that nobody owns.
  • Partner isolation. Third parties never touch internal systems directly. A breach or a misbehaving partner gets caught at the gateway, not three layers deep in the architecture.
  • Regulatory audit trail. Every integration call and every event flows through known mediation layers that log. Compliance questions have a single source of truth.
  • Operational consistency. Every service uses the same auth, observability, and routing primitives. Engineers learn the patterns once and apply them everywhere.
  • Onboarding becomes configuration. Adding a service or a partner is configuration work in known layers, not architecture work that touches the whole system.

The compounding benefit is the key one. Each of these is a one-time investment that pays back on every subsequent integration. The architecture is expensive to set up and cheap to extend, which is exactly the opposite shape of the point-to-point system it replaces.

When Not to Use It

Honest limits. Three cases where this architecture is the wrong choice:

  • You're below the scale where it pays back. If you have under ten services and no external partners, the operational complexity outweighs the integration savings. Use a modular monolith or a small set of services with direct API calls. Revisit the question when you cross ~15 services or onboard your second non-trivial partner.
  • Latency is critical. Every call through the ESB adds a hop. For sub-10ms latency-budgeted workloads (high-frequency trading, real-time bidding, certain game backends), the ESB is the wrong layer. Use direct calls with a service mesh handling cross-cutting concerns at the infrastructure level.
  • You already have a working service mesh. Istio, Linkerd, and similar service-mesh tooling handle several of the same concerns the ESB does — routing, observability, security — at the infrastructure layer. If your mesh is already in place and working, layering an ESB on top adds complexity without adding capability. The decision is mesh-or-ESB, not both.

The bigger meta-warning: this is critical infrastructure. The ESB and the event bus need real SRE attention, real on-call rotation, and real capacity planning. Teams that adopt this architecture without the operational muscle to run it produce systems where the integration layer becomes a single point of failure for the whole business. The boring-architecture-beats-clever-architecture principle applies here unambiguously — adopt this only when you can actually operate it.

Implementation Stack Choices

A short tour of the common production stacks.

ESB options. Mulesoft (commercial, full-featured, the enterprise default for serious deployments). Apache Camel (open source, code-first, popular in Java shops). WSO2 (open source, full enterprise features, common in regulated industries). Cloud-managed options like Azure Service Bus or AWS EventBridge for teams that want managed operations and are willing to accept the cloud-vendor coupling.

Event bus options. Kafka (high throughput, durable, the dominant choice for serious volumes). RabbitMQ (lower throughput, more flexible routing, good for complex topic structures). NATS (lightweight, simple, fast — good for teams that want async without Kafka's operational weight). AWS SQS/SNS (managed, simpler operations, the right choice if you're already deep in AWS).

Decision criteria: throughput requirements, ordering and durability guarantees, team operational expertise with each system, and how much of the operational complexity you want to outsource to a vendor. The right answer for most teams getting started is "managed cloud services for both layers" — accept the lock-in in exchange for radically reduced operational overhead. Migration to self-hosted is possible later if the scale or cost picture demands it.

The Practitioner's Take

ESB + event-driven is the architecture that scales past the point where point-to-point integration breaks. The cost is operational complexity — the bus and the event system are real infrastructure that needs real SRE attention. The benefit is that the cost of every subsequent integration drops dramatically. Adding a service is configuration. Onboarding a partner is configuration. Schema evolution is contained. Compliance has a clean answer.

The teams that adopt this well treat it as critical infrastructure: they instrument it like infrastructure, run it like infrastructure, and budget for it like infrastructure. The teams that adopt it badly treat it as another component and watch it become the single point of failure for the whole business. The choice is rarely about whether to adopt the architecture — at scale, the alternative is worse. The choice is about whether you can actually run it.

For most enterprises sitting in the middle of integration sprawl, the answer is yes, eventually. The right move is to start small — pick one cleanly-isolated subsystem, build the ESB and event-bus skeleton there, prove the operational patterns, and expand from the inside out. The architecture rewards patience and punishes shortcuts. So do most things that scale.