Skip to content
academia.sh

Lesson 06 / 10

The Enterprise Service Bus

Building and comparing the same flow set with point-to-point edges and with a central hub: the number of translators and adapters written, how many places a rule appears, the number of messages the hub processes, the share of flows that break when the hub goes down, the number of owners touched by adding a new system, and the ownership of the business logic that piles up at the hub.

Contents

The previous lesson built a single edge with four patterns and showed what one pattern costs. In the enterprise, though, every system talks to every other system by whatever path it chose. This lesson builds the same set of data flows two ways — a direct edge for each flow, or a single central point that all flows pass through — and compares the two with the same measures.

IN4 — the enterprise is fictional; the flow set is written as an array in node. Every quantity measured is read either from this array, from the running processes, or from the integration code’s own text.

IN5 — the receiver systems are modeled in a single process, each on its own port. Rationale: the quantities measured (units written, hops crossed, messages processed, flows broken) are independent of whether the systems sit on separate machines; separate ports are enough to preserve the system boundary.

IN6 — the hub is not a product, it is a location. What is measured is not which capabilities the hub carries, but what routing every flow through a single point does to edge count, failure surface, and ownership.

The Flow Set

The regional library network has six systems, nine data flows, and five separate owners. The loan service reports every loan to four systems, the membership system reports member changes to three systems, and the catalog reports copy status to two systems. Nine flows are a small subset of the thirty directed pairs six systems could form; the graph is sparse, and this sparseness determines most of the numbers that follow.

// enterprise.mjs — fictional enterprise model: systems, their owners, and the data flows between them
export const OWNER = { catalog: "catalog unit", loan: "in-house development",
  billing: "in-house development", membership: "membership unit", reporting: "management unit",
  "branch-interface": "branch unit", reservation: "branch unit", hub: "integration unit" };
export const RECEIVER = ["billing", "reporting", "catalog", "branch-interface", "loan"];
export const FLOW = [                            // [source, target]; each row is an edge
  ["loan", "billing"], ["loan", "reporting"], ["loan", "catalog"],
  ["loan", "branch-interface"], ["membership", "loan"], ["membership", "billing"],
  ["membership", "reporting"], ["catalog", "branch-interface"], ["catalog", "reporting"],
];
export const NEW = [                             // flows born when the seventh system is added
  ["loan", "reservation"], ["membership", "reservation"], ["catalog", "reservation"],
  ["reservation", "loan"], ["reservation", "branch-interface"],
];
export const degree = (a) => FLOW.filter((b) => b.includes(a)).length;

if (process.argv[1].endsWith("enterprise.mjs")) {
  const s = [...new Set(FLOW.flat())];
  console.log(`${s.length} systems, ${FLOW.length} flows, ${new Set(s.map((a) => OWNER[a])).size} owners; ` +
    `possible directed pairs ${s.length * (s.length - 1)}`);
  console.log(`system touching the most flows: ` +
    `${s.map((a) => `${a} ${degree(a)}`).sort((x, y) => y.split(" ")[1] - x.split(" ")[1])[0]}`);
  console.log(`seventh system added: flows ${FLOW.length} -> ${FLOW.length + NEW.length}`);
}
6 systems, 9 flows, 5 owners; possible directed pairs 30
system touching the most flows: loan 5
seventh system added: flows 9 -> 14

The receiver systems sit in a single process on five separate ports and write the record they receive to their own log; the two arrangements’ results are compared from these logs.

// systems.mjs — five receiver systems, each on its own port; <base-port>; long-lived process
import { createServer } from "node:http";
import { appendFileSync } from "node:fs";
import { RECEIVER } from "./enterprise.mjs";
const [base] = process.argv.slice(2);
if (!base) { console.log("usage: node systems.mjs <base-port>"); process.exit(1); }
RECEIVER.forEach((name, i) => createServer(async (request, response) => {
  let s = "";
  for await (const p of request) s += p;
  appendFileSync(`received-${name}.log`, `${s}\n`);
  response.end("done");
}).listen(Number(base) + i));

Point-to-Point Edges

In the point-to-point arrangement, one translator is written for each flow, and the translator lives inside the source system. Every system has its own vocabulary: the loan service says memberId, the membership system says member_no, billing says memberNo. The translator closes this gap and produces the fields the target expects. Four rules (late fee, report period, branch region, member class) are called inside the translators.

// point-to-point.mjs — point-to-point integration: one translator per flow; the translator lives in the source system
export const lateFee = (days) => days * 5;                    // late fee rule
export const period = (time) => time.slice(0, 7);             // report period rule
export const region = (code) => (code <= 2 ? "north" : "south");   // branch region rule
export const memberClass = (statusCode) => (statusCode === 1 ? "full" : "restricted");   // member class rule
export const TRANSLATOR = {
  "loan->billing": (o) => ({ memberNo: o.memberId, copy: o.copyId, debt: lateFee(o.daysLate) }),
  "loan->reporting": (o) => ({ event: "loan", key: o.copyId, period: period(o.time) }),
  "loan->catalog": (o) => ({ copyId: o.copyId, available: false }),
  "loan->branch-interface": (o) => ({ copy: o.copyId, region: region(o.branchCode) }),
  "membership->loan": (o) => ({ memberId: o.member_no, memberClass: memberClass(o.status_code) }),
  "membership->billing": (o) => ({ memberNo: o.member_no, memberClass: memberClass(o.status_code) }),
  "membership->reporting": (o) => ({ event: "member", key: o.member_no, period: period(o.time) }),
  "catalog->branch-interface": (o) => ({ copy: o.copyId, region: region(o.branchCode) }),
  "catalog->reporting": (o) => ({ event: "copy", key: o.copyId, period: period(o.time) }),
};

The Hub

In the hub arrangement, the producing system hands its own format to a single place. The hub converts that format to the common format, finds the targets by looking at the flow table, and for each target converts to and sends that system’s format. There are two adapters per system: inbound and outbound. The four rules now live in a single place, and the RULE array records who actually owns each rule’s work.

// hub.mjs — the central point; <port> <base-port>; long-lived process
import { createServer } from "node:http";
import { FLOW, RECEIVER } from "./enterprise.mjs";
const [port, base] = process.argv.slice(2);
if (!base) { console.log("usage: node hub.mjs <port> <base-port>"); process.exit(1); }
const lateFee = (days) => days * 5, period = (time) => time.slice(0, 7);   // rules collected at the hub
const region = (code) => (code <= 2 ? "north" : "south"), memberClass = (statusCode) => (statusCode === 1 ? "full" : "restricted");
export const RULE = [                            // rule at the hub and the actual owner of the work
  { name: "lateFee", owner: "in-house development" }, { name: "period", owner: "management unit" },
  { name: "region", owner: "branch unit" }, { name: "memberClass", owner: "membership unit" },
];
const INBOUND = {                                // system format -> common format
  loan: (o) => ({ type: "loan", member: o.memberId, copy: o.copyId, branch: o.branchCode,
    late: o.daysLate, time: o.time }),
  membership: (o) => ({ type: "member", member: o.member_no, status: o.status_code, time: o.time }),
  catalog: (o) => ({ type: "copy", copy: o.copyId, branch: o.branchCode, time: o.time }),
};
const OUTBOUND = {                               // common format -> system format
  billing: (c) => (c.type === "loan"
    ? { memberNo: c.member, copy: c.copy, debt: lateFee(c.late) }
    : { memberNo: c.member, memberClass: memberClass(c.status) }),
  reporting: (c) => ({ event: c.type, key: c.copy ?? c.member, period: period(c.time) }),
  catalog: (c) => ({ copyId: c.copy, available: false }),
  "branch-interface": (c) => ({ copy: c.copy, region: region(c.branch) }),
  loan: (c) => ({ memberId: c.member, memberClass: memberClass(c.status) }),
};
let incoming = 0, outgoing = 0;                  // number of messages the hub has processed

createServer(async (request, response) => {
  if (request.url === "/counts") { response.end(`${incoming} ${outgoing}`); return; }
  if (request.url === "/shutdown") { response.end("closed"); process.exit(0); }
  let s = "";
  for await (const p of request) s += p;
  const { source, body } = JSON.parse(s);
  incoming += 1;
  const common = INBOUND[source](body);
  for (const t of FLOW.filter((b) => b[0] === source).map((b) => b[1])) {
    outgoing += 1;
    await fetch(`http://127.0.0.1:${Number(base) + RECEIVER.indexOf(t)}/receive`,
      { method: "POST", body: JSON.stringify(OUTBOUND[t](common)) });
  }
  response.end("done");
}).listen(Number(port));

Measurement

The tool runs the same six events first through the point-to-point arrangement, then through the hub arrangement, and compares the receiver logs. The number of units written and the number of places a rule appears are read from the text of the two integration files, not from the running processes. In the final step the hub is shut down and the same six events are sent again.

// measure.mjs — measures two arrangements on the same flow set; <base-port> <hub-port>
import { existsSync, readFileSync, rmSync } from "node:fs";
import { FLOW, RECEIVER, OWNER, NEW } from "./enterprise.mjs";
import { TRANSLATOR } from "./point-to-point.mjs";
const [base, hp] = process.argv.slice(2);
if (!hp) { console.log("usage: node measure.mjs <base-port> <hub-port>"); process.exit(1); }
const EVENT = [                                  // events in the producing systems' own formats
  ["loan", { memberId: 7, copyId: 401, branchCode: 1, daysLate: 3, time: "2026-04-11" }],
  ["loan", { memberId: 9, copyId: 402, branchCode: 3, daysLate: 0, time: "2026-04-12" }],
  ["membership", { member_no: 7, status_code: 1, time: "2026-04-11" }],
  ["membership", { member_no: 9, status_code: 2, time: "2026-04-13" }],
  ["catalog", { copyId: 401, branchCode: 1, time: "2026-04-11" }],
  ["catalog", { copyId: 403, branchCode: 4, time: "2026-04-14" }],
];
const DELIVERIES = FLOW.length * 2;              // each flow carries two events
const clear = () => RECEIVER.forEach((a) => rmSync(`received-${a}.log`, { force: true }));
const collect = () => RECEIVER.flatMap((a) => existsSync(`received-${a}.log`)
  ? readFileSync(`received-${a}.log`, "utf8").split("\n").filter(Boolean).map((s) => `${a} ${s}`) : []);
const runPointToPoint = async () => {            // the translator runs in the source system, sends directly
  for (const [source, native] of EVENT) {
    for (const t of FLOW.filter((b) => b[0] === source).map((b) => b[1])) {
      await fetch(`http://127.0.0.1:${Number(base) + RECEIVER.indexOf(t)}/receive`,
        { method: "POST", body: JSON.stringify(TRANSLATOR[`${source}->${t}`](native)) });
    }
  }
};
const runHub = async () => {                     // the producer hands only its own format to the hub
  let accepted = 0;
  for (const [source, native] of EVENT) {
    try {
      await fetch(`http://127.0.0.1:${hp}/event`, { method: "POST",
        body: JSON.stringify({ source, body: native }) });
      accepted += 1;
    } catch { accepted += 0; }                   // producer cannot deliver while the hub is down
  }
  return accepted;
};
clear(); await runPointToPoint(); const a = collect().sort();
clear(); await runHub(); const b = collect().sort();
const [incoming, outgoing] = (await (await fetch(`http://127.0.0.1:${hp}/counts`)).text()).split(" ").map(Number);
const hubSrc = readFileSync("hub.mjs", "utf8"), p2pSrc = readFileSync("point-to-point.mjs", "utf8");
const section = (m, name) => m.split(`const ${name} = {`)[1].split("\n};")[0];
const unitCount = (m) => m.match(/^ {2}"?[\w>-]+"?: \(/gm).length;   // units written per pattern
const calls = (m) => ["lateFee", "period", "region", "memberClass"]  // number of places the rule appears
  .reduce((s, r) => s + (m.match(new RegExp(`\\b${r}\\(`, "g")) || []).length, 0);
const rule = hubSrc.split("RULE = [")[1].split("];")[0].match(/owner: "([^"]+)"/g);
const setSize = (l) => new Set(l).size;
const HUB_UNITS = ["reservation", "hub"];        // units touched in the hub arrangement
const row = (name, x, y) => console.log(`${name.padEnd(41)}${String(x).padStart(7)}${String(y).padStart(9)}`);

row("measure", "p2p", "hub");
row("flow (edge) count", FLOW.length, FLOW.length);
row("translators / adapters written", unitCount(section(p2pSrc, "TRANSLATOR")),
  unitCount(section(hubSrc, "INBOUND")) + unitCount(section(hubSrc, "OUTBOUND")));
row("places the rule appears", calls(p2pSrc), calls(hubSrc));
row("hops per delivery", 1, 2);
row("messages the busiest unit handles", 2, incoming + outgoing);
row("flows broken if one unit goes down", Math.max(...FLOW.flat()
  .map((s) => FLOW.filter((b) => b.includes(s)).length)), FLOW.length);
row("seventh system: units to write", NEW.length, HUB_UNITS.length);
row("seventh system: owners touched", setSize(NEW.map((b) => OWNER[b[0]])),
  setSize(HUB_UNITS.map((s) => OWNER[s])));
row("rules at the hub owned by someone else", 0,
  rule.filter((s) => !s.includes(OWNER.hub)).length);
console.log(`both arrangements' deliveries: ${a.length}/${b.length} records, ` +
  `identical: ${JSON.stringify(a) === JSON.stringify(b) ? "yes" : "no"}`);

await fetch(`http://127.0.0.1:${hp}/shutdown`).catch(() => 0);     // the hub is shut down
clear(); const accepted = await runHub(); const downCount = collect().length;
clear(); await runPointToPoint();
console.log(`hub down: ${accepted}/${EVENT.length} events accepted, ` +
  `${downCount}/${DELIVERIES} delivered; point-to-point delivered ${collect().length}/${DELIVERIES} at the same time`);
node systems.mjs 8991 & S=$!
node hub.mjs 8990 8991 &
sleep 0.8
node measure.mjs 8991 8990
kill $S
measure                                      p2p      hub
flow (edge) count                              9        9
translators / adapters written                 9        8
places the rule appears                        8        5
hops per delivery                              1        2
messages the busiest unit handles              2       24
flows broken if one unit goes down             5        9
seventh system: units to write                 5        2
seventh system: owners touched                 4        2
rules at the hub owned by someone else         0        4
both arrangements' deliveries: 18/18 records, identical: yes
hub down: 0/6 events accepted, 0/18 delivered; point-to-point delivered 18/18 at the same time

How Much the Edge Count Drops

The comparison before the last line establishes the measurement’s validity: both arrangements deliver eighteen records, and the delivered records are exactly identical. The difference is not in the outcome of the work, but in where the work sits.

The second row does not give the expected payoff. Nine translators become eight adapters for nine flows; the gain is a single unit. The classic justification for hub-based integration is that edge count grows with the square of the system count, but that justification rests on the assumption that every system talks to every other system. In this enterprise, only nine of the thirty directed pairs six systems could form actually exist. Translator count grows with the number of flows, adapter count grows with the number of systems; as long as the graph is sparse the two stay close together, and the hub arrangement’s advantage on this row is small.

The real gain is in the third row. The same four rules are called eight times in the point-to-point arrangement, and five times at the hub. The report period rule appears in three separate translators, because all three systems that send data to reporting have to do that calculation in their own code. If the period definition changes, the point-to-point arrangement requires three separate systems to change; the hub arrangement requires one unit. What the hub sells is not a lower edge count, but the removal of repetition.

The Hub’s Cost

The fifth row turns the bottleneck into a number. For six events the hub processes twenty-four messages: six inbound, eighteen outbound. In the point-to-point arrangement, the busiest single edge processes two messages. The hub does twelve times the work of the busiest point-to-point edge, and that ratio grows with the number of flows; on top of that, it adds one hop per delivery.

The sixth row gives the failure surface. In the point-to-point arrangement, the system touching the most flows is the loan service, and when it goes down, five of the nine flows break. When the hub goes down, all nine of nine break. The output’s last line shows this by measurement: when the hub is shut down, none of the six events are accepted and zero of eighteen deliveries happen; at the same time, the point-to-point arrangement completes eighteen of eighteen deliveries. When the loan service goes down, the enterprise has already stopped, because it cannot lend books anyway; when the hub goes down, a component that does no business work of its own stops the entire enterprise. This is the unsettling part of a single point of failure: the component with zero business value has the highest share of failure.

A New System and the Logic That Piles Up at the Hub

The seventh and eighth rows are the hub arrangement’s strongest defense. Adding the reservation system creates five new flows. In the point-to-point arrangement, five translators are written for these five flows, and because the translators live inside the source systems, the loan, membership, catalog, and reservation code all get touched: schedules have to be reconciled with four separate owners. In the hub arrangement only two units are written — reservation’s inbound and outbound adapters — and the number of owners touched drops to two. The new system’s cost decouples from the system count.

The last row shows what this costs in return. Four rules sit at the hub, and all four have a different actual owner: late fee is billing’s business, report period is management’s, region definition is the branch unit’s, member class is membership’s. The integration unit that owns the hub has none of the expertise to write any of these rules, but it carries all of their code. This has two consequences. First, when the member class definition changes, the change ships in the hub’s release, not the membership unit’s own release; second, that release redeploys all nine flows at once. As the rule’s repetition disappears, the rule’s ownership blurs, and the deployment units become coupled to each other. The choice depends on how tight the flow graph is: the hub lowers repetition and the coordination cost of a new system, and in exchange adds one hop, a shared point of failure, and a pile of rules with unclear ownership.

Summary

  • The same nine flows were built in both arrangements, and both delivered the same 18 records with exactly identical content; the comparison was built on top of this verification.
  • The number of units written is 9 versus 8; since only 9 of six systems’ 30 possible directed pairs exist, the hub’s gain in edge count stayed at a single unit.
  • The same four rules appear in 8 places in the point-to-point arrangement and 5 at the hub; the report period rule is repeated in three separate systems.
  • The hub processes 24 messages for six events, versus 2 for the busiest point-to-point edge; when the hub is shut down, 0 of 18 deliveries happen while the point-to-point arrangement completes all 18 at the same time.
  • The seventh system costs 5 units and 4 owners in the point-to-point arrangement, and 2 units and 2 owners in the hub arrangement; in exchange, 4 rules owned by someone else pile up at the hub.

Next Step

Across two lessons, data always flowed in one direction: one system wrote, the others read. The source was fixed, the edge’s direction was fixed, and there was no such thing as conflict. In the enterprise, this assumption does not hold for most entities. A member record lives in the membership system, in the loan service, and in the municipality’s identity service; all three are written from their own user interface, and all three carry different values in the address field for the same member. The next lesson holds the same entity in three systems and counts: how many systems write, how many fields conflict, how many records a reconciliation round fixes, and how many inconsistencies remain; how these numbers change when one system is chosen as the source, and what that choice lengthens in the write path.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close