~60 min read · updated 2026-05-15

Adding an enterprise service bus

WSO2 Micro Integrator as a second podman container on a shared network. One synapse API, name-based routing, and end-to-end verification.

This is where the project gets its first integration surface. We add a second container to the VM — a WSO2 Micro Integrator runtime — and route HTTP traffic through it into the Liberty app. Same VM, same git repo, separate runtime.

What an ESB is (and isn’t) in 2026

The “enterprise service bus” pattern came out of mid-2000s SOA. Today most teams reach for it when:

  • Outbound integrations get noisy enough to deserve a separate config tree, a separate release cadence, and a separate ops surface from the main application.
  • Multiple consuming services need the same routing/transformation logic, and embedding it in each one would mean copying.
  • A central place to attach API gateway concerns (auth, throttling, logging, observability) buys more than it costs.

What an ESB isn’t is a free lunch. It is a second runtime to deploy, monitor, and version. For a single application with a couple of HTTP integrations, Apache Camel embedded in the Liberty app is often a better fit. Pick the ESB when the separation pays for itself.

The shortlist

OptionLicenseFootprintWhen it fits
Apache Camel (embedded)Apache 2.0Library, in-processRouting logic that belongs in the app
WSO2 Micro IntegratorApache 2.0~150 MB image, ~5 s startupContainer-native ESB, separate runtime
MuleSoft AnypointCommercialSeparate Mule runtime + Anypoint platformAn integration practice, not one app
IBM App Connect EnterpriseCommercialHeavier stillIBM Cloud Paks shop

We pick WSO2 MI. It’s actually open-source (not a feature-gated community edition), it’s container-native, and it runs cleanly side-by-side with our Liberty container.

The runtime topology

Two containers, one shared user-defined podman network. The Liberty app keeps its 9080/9443. The MI listens on 8290/8253. Both publish to the host so we can curl from the VM shell for smoke tests, but inside the network they refer to each other by container name.

laptop ──ssh──> hypervisor ──ProxyJump──> VM
                                          ├── container: insurance-app   :9080
                                          └── container: insurance-mi    :8290

                                              └── insurance-net (10.89.0.0/24)

The Containerfile for MI

FROM docker.io/wso2/wso2mi:4.4.0

# Drop our synapse configs into the default config dir.
COPY --chown=wso2carbon:wso2 synapse-config/api/ \
     /home/wso2carbon/wso2mi-4.4.0/repository/deployment/server/synapse-configs/default/api/

EXPOSE 8290 8253

That’s it. The base image starts MI when the container starts, and any synapse XML you drop into synapse-configs/default/<kind>/ is loaded at boot.

One synapse API

mi/synapse-config/api/InsurancePingAPI.xml:

<?xml version="1.0" encoding="UTF-8"?>
<api context="/insurance" name="InsurancePingAPI"
     xmlns="http://ws.apache.org/ns/synapse">
    <resource methods="GET" uri-template="/ping">
        <inSequence>
            <call>
                <endpoint>
                    <http method="get"
                          uri-template="http://insurance-app:9080/api/ping"/>
                </endpoint>
            </call>
            <respond/>
        </inSequence>
        <faultSequence/>
    </resource>
</api>

<call> + <respond/> is the modern synchronous request/response pattern in synapse — cleaner than the older <send/> + <outSequence> style. The endpoint URI uses the container name insurance-app, not an IP — possible because both containers attach to the same podman network with DNS enabled.

Wiring it up

# Create the shared network (once)
podman network create insurance-net

# Build the MI image
cd ~/insurance-app
podman build --network=host -t insurance-mi:dev -f mi/Containerfile mi/

# (Re)start both containers on the shared network
podman run -d --replace --name insurance-app --network insurance-net \
  -p 9080:9080 -p 9443:9443 insurance-app:dev

podman run -d --replace --name insurance-mi --network insurance-net \
  -p 8290:8290 -p 8253:8253 insurance-mi:dev

The --replace flag is gold — it stops any existing container with the same name and starts the new one, no manual podman rm ceremony.

Verifying the path

# Direct to Liberty
curl http://localhost:9080/api/ping
# {"status":"ok"}

# Through MI
curl http://localhost:8290/insurance/ping
# {"status":"ok"}

Both return the same JSON. The second one took a slightly longer path: laptop → SSH → VM → MI container → bridge network → Liberty container → and back.

A topology nuance: name-based routing

Switching the network from --network host to a user-defined bridge (insurance-net) is what enabled http://insurance-app:9080/... in the synapse config. Podman’s user-defined bridges have embedded DNS; the default podman bridge does not, and --network host doesn’t even have an isolated network at all. If you ever switch back to host, the synapse endpoint URI must change to localhost:9080 and MI loses the cleaner abstraction.

What you have

  • Two containers running side by side under one rootless podman.
  • A shared bridge network with name-based routing.
  • One synapse API that demonstrates the ESB pattern end-to-end.
  • A platform you can extend by dropping more synapse files into mi/synapse-config/.

From here, the rest of the track adds the real-world dependencies an insurance backend would actually lean on: database, cache, event bus, identity, gateway, observability — one container per module.

Next: 07 — Persistence →