---
title: 'Gateway Routing and Aggregation'
source: 'https://academia.sh/en/courses/traffic-layer/gateway-routing-and-aggregation'
course: 'The Traffic Layer'
language: en
updated: '2026-08-23T07:01:36+00:00'
license: 'CC BY-SA 4.0'
---

# Gateway Routing and Aggregation

The service map's two separate jobs: converting a path to a single service, and spreading one request across several services and merging the responses into one body; measuring three access patterns with real local processes and calculating how far aggregation cuts the request count while it grows the body at the edge.

The previous lesson said the gateway carries a service map but left the map as an assumption.
The map can do two separate jobs. The first is **gateway routing**: an incoming path translates
to a single service, and the client never sees the split behind it. The second is **gateway
aggregation**: a single request spreads across several services, and the responses that come back
merge into one body.

The concrete case of this distinction is the tracking page. The page takes a shipment's state
from the delivery operations context and its fee from the billing context. Two contexts are two
separate services, so the page needs two responses. This lesson measures how many requests, how
many bytes, and how many rounds those two responses take with real local processes; then it
applies the measured ratio to K01's Back-of-the-Envelope Estimation lesson's tracking-response
count.

## Mechanism

Two domain services and one gateway run as separate processes. The field set the services return
was determined by the resource design rules in the Web API Design course and is not revisited
here; what gets measured is the decision made at the edge.

```js
// gateway/service.mjs — two domain services: delivery-ops 8471, billing 8472
import { createServer } from "node:http";

const STATE = {
  "TR-4821": { state: "out-for-delivery", zone: "35", updatedAt: "2026-03-11T08:24:00Z",
    route: ["34", "41", "35"] },
  "TR-9007": { state: "accepted", zone: "34", updatedAt: "2026-03-11T07:02:00Z", route: ["34"] },
};
const FEE = {
  "TR-4821": { tariff: 9600, discount: 1440, net: 8160, contractNo: "S7" },
};

const serve = (port, name, table) => createServer((request, response) => {
  const no = request.url.split("/").pop();
  const record = table[no];
  const body = JSON.stringify(record ? { trackingNo: no, ...record } : { error: "not_found" });
  response.writeHead(record ? 200 : 404, { "Content-Type": "application/json" });
  response.end(body);
}).listen(port, "127.0.0.1", () => console.log(`${name} 127.0.0.1:${port}`));

serve(8471, "delivery-ops ", STATE);
serve(8472, "billing      ", FEE);
```

The gateway holds the map as an object and separates two paths: `/tracking/<no>/<sub>` converts
to a single service, while `/tracking/<no>` calls two services in one round. A third path,
`/counter`, reads and resets the counters between the gateway and the services; it is a
measurement tip, not part of the design.

```js
// gateway/gateway.mjs — API gateway 8473: routing and single-request aggregation via the service map
import { createServer } from "node:http";

const MAP = { state: 8471, fee: 8472 };           // path segment -> service
let counter = { requests: 0, bytes: 0, round: 0 }; // counters between gateway and services

async function callService(name, no) {
  counter.requests += 1;
  const y = await fetch(`http://127.0.0.1:${MAP[name]}/${name}/${no}`);
  const text = await y.text();
  counter.bytes += Buffer.byteLength(text);
  return y.ok ? JSON.parse(text) : null;
}

createServer(async (request, response) => {
  const [root, no, sub] = request.url.split("/").filter(Boolean);
  let body;
  if (root === "counter") {                          // measurement tip: reads and resets counters
    body = counter;
    counter = { requests: 0, bytes: 0, round: 0 };
  } else if (root !== "tracking") {
    body = null;
  } else if (sub) {                                   // routing: path converts to a single service
    counter.round += 1;
    body = await callService(sub, no);
  } else {                                             // aggregation: two services called in one round
    counter.round += 1;
    const [state, fee] = await Promise.all([callService("state", no), callService("fee", no)]);
    body = state && { ...state, fee: fee && { tariff: fee.tariff,
      discount: fee.discount, net: fee.net, contractNo: fee.contractNo } };
  }
  const text = JSON.stringify(body ?? { error: "not_found" });
  response.writeHead(body ? 200 : 404, { "Content-Type": "application/json" });
  response.end(text);
}).listen(8473, "127.0.0.1", () => console.log("gateway       127.0.0.1:8473"));
```

The client tries the three access patterns in turn and collects six numbers for each. Byte
counts count only the body; header and protocol overhead were left out of K01's calculation too.

```js
// gateway/measure.mjs — measures the three access patterns and ties them to K01's tracking-response calculation
const get = async (url) => {
  const y = await fetch(url);
  const text = await y.text();
  return { bytes: Buffer.byteLength(text), data: y.ok ? JSON.parse(text) : null };
};
const readCounter = async () => (await get("http://127.0.0.1:8473/counter")).data;
const leaf = (d) => (typeof d !== "object" || d === null ? 1
  : Object.values(d).reduce((t, v) => t + leaf(v), 0));

async function measure(name, addresses, requests) {
  await readCounter();                                 // resets the previous scenario's counters
  let bytes = 0, fields = 0;
  for (const u of requests) {
    const r = await get(u);
    bytes += r.bytes;
    fields += r.data ? leaf(r.data) : 0;
  }
  const internal = await readCounter();
  return { name, "client requests": requests.length, "addresses the client knows": addresses,
    "bytes reaching the client": bytes, "fields carried": fields,
    "gateway-service requests": internal.requests, "gateway-service bytes": internal.bytes,
    "gateway rounds": internal.round };
}

const N = "TR-4821";
const S = [
  await measure("separate calls", 2, [`http://127.0.0.1:8471/state/${N}`, `http://127.0.0.1:8472/fee/${N}`]),
  await measure("routing", 1, [`http://127.0.0.1:8473/tracking/${N}/state`,
    `http://127.0.0.1:8473/tracking/${N}/fee`]),
  await measure("aggregation", 1, [`http://127.0.0.1:8473/tracking/${N}`]),
];
const FIELD = ["client requests", "addresses the client knows", "bytes reaching the client",
  "fields carried", "gateway-service requests", "gateway-service bytes", "gateway rounds"];
console.log(`${"measure".padEnd(26)}${S.map((s) => s.name.padStart(15)).join("")}`);
for (const f of FIELD) console.log(`${f.padEnd(26)}${S.map((s) => String(s[f]).padStart(15)).join("")}`);

// Missing piece: billing does not know this tracking number
const missing = await get(`http://127.0.0.1:8473/tracking/TR-9007`);
console.log(`\naggregation with a missing piece = ${missing.bytes} bytes, fields carried = ${leaf(missing.data)}`);
console.log(`  body: ${JSON.stringify(missing.data)}`);

// K01 Back-of-the-Envelope Estimation: V5 = 480 bytes (assumption), read peak 416.67 requests/s (computed value)
const stateBytes = (await get(`http://127.0.0.1:8471/state/${N}`)).bytes;
const feeBytes = (await get(`http://127.0.0.1:8472/fee/${N}`)).bytes;
const combined = (await get(`http://127.0.0.1:8473/tracking/${N}`)).bytes;
const V5 = 480, READ_PEAK = 416.67;
const mbit = (requests, bytes) => (requests * bytes * 8) / 1e6;
console.log(`\nstate ${stateBytes} B, fee ${feeBytes} B, combined ${combined} B`);
console.log(`ratio — fee/state = ${(feeBytes / stateBytes).toFixed(3)}, ` +
  `combined/state = ${(combined / stateBytes).toFixed(3)}`);
const SETUP = [
  ["K01 (state only)", READ_PEAK, V5],
  ["separate calls", READ_PEAK * 2, (V5 * (stateBytes + feeBytes)) / stateBytes / 2],
  ["aggregation", READ_PEAK, (V5 * combined) / stateBytes],
];
console.log(`${"setup".padEnd(20)}${"requests/s".padStart(12)}${"bytes".padStart(9)}${"Mbit/s".padStart(9)}`);
for (const [name, requests, bytes] of SETUP) {
  console.log(`${name.padEnd(20)}${requests.toFixed(2).padStart(12)}${bytes.toFixed(0).padStart(9)}` +
    `${mbit(requests, bytes).toFixed(2).padStart(9)}`);
}
const [, separate, agg] = SETUP;
console.log(`aggregation / separate calls — requests x${(agg[1] / separate[1]).toFixed(2)}, ` +
  `egress x${(mbit(agg[1], agg[2]) / mbit(separate[1], separate[2])).toFixed(2)}`);

// T1 sensitivity: if only a portion of views ask for the fee (T1 = 0.40)
const T1 = 0.4;
const separateRequests = READ_PEAK * (1 + T1);
const separateBytes = (V5 * (stateBytes + T1 * feeBytes)) / stateBytes / (1 + T1);
const aggBytes = (V5 * (stateBytes + T1 * (combined - stateBytes))) / stateBytes;
console.log(`T1 = ${T1.toFixed(2)} -> separate calls ${separateRequests.toFixed(2)} requests/s ` +
  `${mbit(separateRequests, separateBytes).toFixed(2)} Mbit/s | aggregation ${READ_PEAK.toFixed(2)} requests/s ` +
  `${mbit(READ_PEAK, aggBytes).toFixed(2)} Mbit/s`);
```

```bash
node gateway/service.mjs & s=$!
curl -s --retry 20 --retry-connrefused --retry-delay 0 -o /dev/null http://127.0.0.1:8472/fee/TR-4821
node gateway/gateway.mjs & g=$!
curl -s --retry 20 --retry-connrefused --retry-delay 0 -o /dev/null http://127.0.0.1:8473/counter
node gateway/measure.mjs
kill $s $g
```

```
delivery-ops  127.0.0.1:8471
billing       127.0.0.1:8472
gateway       127.0.0.1:8473
measure                    separate calls        routing    aggregation
client requests                         2              2              1
addresses the client knows              2              1              1
bytes reaching the client             206            206            190
fields carried                         12             12             11
gateway-service requests                0              2              2
gateway-service bytes                   0            206            206
gateway rounds                          0              2              1

aggregation with a missing piece = 116 bytes, fields carried = 6
  body: {"trackingNo":"TR-9007","state":"accepted","zone":"34","updatedAt":"2026-03-11T07:02:00Z","route":["34"],"fee":null}

state 123 B, fee 83 B, combined 190 B
ratio — fee/state = 0.675, combined/state = 1.545
setup                 requests/s    bytes   Mbit/s
K01 (state only)          416.67      480     1.60
separate calls            833.34      402     2.68
aggregation               416.67      741     2.47
aggregation / separate calls — requests x0.50, egress x0.92
T1 = 0.40 -> separate calls 583.34 requests/s 2.03 Mbit/s | aggregation 416.67 requests/s 1.95 Mbit/s
```

## What Routing Alone Gains

The table's first two columns answer this. Once routing is in place, client requests stay at 2,
bytes reaching the client stay at 206, and fields carried stay at 12. The only number that
changes is addresses the client knows: 2 instead of 1. Routing is not a performance decision, it
is a **binding** decision. Because the service map sits at the gateway, when the billing service
moves to a different address or splits into two, the client is not touched; the map is.

In exchange, gateway-service requests rise from 0 to 2. The same two calls still happen, only
behind the gateway now. Routing does not eliminate a call; it changes who makes it.

## What Aggregation Changes

The third column moves four numbers at once. Client requests drop from 2 to 1, fields carried
from 12 to 11, bytes reaching the client from 206 to 190, and gateway rounds from 2 to 1. The
field count drops because `trackingNo` moves from being carried in two bodies to one; that is
also where the byte reduction comes from. The gateway round dropping to 1 is because the two
calls happen in one round without waiting on each other; the side waiting for both services to
respond is the gateway, not the client.

The number that stays the same matters: gateway-service requests is still 2 under aggregation.
Aggregation does not reduce how many calls the system makes — it reduces the call count **visible
outside the edge**. This distinction decides where the decision gets written: the gain sits in
the client's round count and in the information the client carries, the cost sits in the
gateway's code waiting on two responses.

## A Missing Piece Is a Contract Decision

Shipment `TR-9007` has no billing record. Aggregation returns a 116-byte body and 6 fields in
this case; the `fee` field is `null`. This is a choice in the code, and its alternatives are
countable: the merged response can return with the missing piece, it can be treated as a full
failure, or the missing piece can be reported to the client in a separate field. A gateway that
aggregates has to make this choice, because a single response now holds the state of more than
one source. Left unwritten, the client can confuse a missing `fee` field with "the fee is zero."
The decision itself does not belong to the gateway — it belongs to the response shape the two
sides have agreed on.

## Back to the Calculation

K01's assumption table counted V5, the tracking response, as 480 bytes, with the reasoning "state,
zone, update time, and the last three route steps" — that is, the state piece alone. The tracking
page also showing the fee was not in K01; it is **this course's assumption** (T1 = 1.00: every
tracking view also asks for fee information). The reasoning is that the page shows two pieces of
information on one screen; its sensitivity is calculated below.

The ratios measured from the mechanism are deterministic: bodies are JSON text, and byte counts
are independent of the machine. The fee body is 0.675 times the state body, the combined body
1.545 times. Applying these ratios to V5 produces three setups. K01's own row is 416.67
requests/s and 480 bytes at 1.60 Mbit/s. With separate calls the request rate doubles (833.34)
and the average body per request falls to 402 bytes; egress is 2.68 Mbit/s. With aggregation the
request rate stays at 416.67 but the body rises to 741 bytes; egress is 2.47 Mbit/s.

Three numbers say three separate things. First, aggregation cuts the request rate in half (×0.50)
but only cuts egress to ×0.92: what determines bandwidth is not packaging but the set of fields
carried. Second, K01's 1.60 Mbit/s is exceeded under both setups; once a requirement grows, the
calculation is rerun, the old number is not kept. Third, T1's sensitivity: if only forty percent
of views ask for the fee, separate calls come out to 583.34 requests/s and 2.03 Mbit/s,
aggregation to 416.67 requests/s and 1.95 Mbit/s. Aggregation's gain in request rate shrinks as T1
shrinks; so does its cost in body growth.

## Summary

- The service map does two jobs: routing converts a path to a single service, aggregation spreads
  one request across several services and merges the responses.
- Routing is only a binding decision: client requests stay at 2, bytes at 206, and fields at 12,
  while addresses the client knows drop from 2 to 1 and gateway-service requests rise from 0 to 2.
- Aggregation cuts client requests from 2 to 1, fields carried from 12 to 11, and gateway rounds
  from 2 to 1; the request count between the gateway and the services stays at 2.
- The body aggregation returns merges the state of more than one source; how a missing piece is
  reported is a contract decision — in the example it came back as `null`, with a 116-byte body
  and 6 fields.
- Measured ratios: the fee body is 0.675 times the state body, the combined body 1.545 times;
  byte counts are independent of the machine.
- Back to K01: the 416.67 requests/s and 480-byte, 1.60 Mbit/s egress becomes 833.34 requests/s
  and 2.68 Mbit/s with separate calls, 416.67 requests/s and 2.47 Mbit/s with aggregation —
  requests ×0.50, egress ×0.92.

## Next Step

Aggregation was the first job that touched the body, and it required a contract decision. Most of
the work that can move to the edge never touches the body at all: validating a token, applying a
rate limit, writing an access log entry. This work can either repeat at every service or gather in
one place; the first lesson compared the two by line count but did not turn which work is
movable into a rule. The next lesson builds that rule: once the inputs a check reads are written
down, the work that can move to the edge separates from the work that must stay at the service by
mechanical means — and it measures what having validation done in a single place costs the
application.
