---
title: 'Communication Styles'
source: 'https://academia.sh/en/courses/application-layer/communication-styles'
course: 'The Application Layer and Service Interaction'
language: en
updated: '2026-08-23T07:01:23+00:00'
license: 'CC BY-SA 4.0'
---

# Communication Styles

Writing the same internal call in remote procedure call, resource-based, and query-based styles and comparing them with three measures specific to the service boundary: the number of internal calls made for one external request, the bytes crossing the internal boundary, and the number of names the caller has to know; multiplying the internal byte volume by the introductory course's peak read rate to convert it into internal boundary bandwidth.

The address is now found at runtime and the list keeps itself current. The pieces needed for two
services to reach each other are complete; but what they say once they reach each other was never
chosen. The Traffic Layer course's gateway called the delivery operations service and the billing
service in parallel and merged their responses into a single body, and the shape of those two
calls was never discussed anywhere.

Three styles were established in the Web API Design course: **remote procedure call**,
**resource-based**, and **query-based**. That course compared the three at the **client
boundary**, and its measures belonged there — request count, the over-fetching rate, which side
has to change when a requirement changes. The service boundary is a different place: both ends are
ours, over-fetching is a defect that can be fixed by hand, and there is no client-version-lag
problem. Protocol and schema language are not retold in this lesson; what is measured is the
choice's three consequences on the system — the **internal call count** for one external request,
the **bytes crossing the internal boundary**, and the number of **names** the caller has to know.

## Setup

Two domain services are separate processes and serve the same data in three styles. The domain
sets carry the two contexts established in M18: delivery operations carries the route and carrier
information, billing carries the tariff and contract information. The fields the gateway reads
from the response come from K01's V5 assumption — state, zone, update time, the last route steps —
plus the fee's net amount added to that.

```js
// communication/service.mjs — two domain services (delivery-ops 8471, billing 8472),
// each serving the same data three ways: resource-based, remote procedure call, and query-based.
import { createServer } from "node:http";

const SHIPMENT = { "TR-4821": { trackingNo: "TR-4821", state: "out-for-delivery", zone: "35",
  updatedAt: "2026-03-11T08:24:00Z", route: ["34", "41", "35"], carrier: "T3",
  weightGrams: 2400, volumeDm3: 12, contractNo: "S7", originBranch: "34-021", destinationBranch: "35-114" } };
const FEE = { "TR-4821": { trackingNo: "TR-4821", tariff: 9600, discount: 1440, net: 8160,
  contractNo: "S7", period: "2026-03", lineItems: 3, taxRate: 0.2 } };

const PROCEDURE = {                                // purpose-built, server-fixed responses
  trackingSummary: (r) => ({ state: r.state, zone: r.zone, updatedAt: r.updatedAt, route: r.route }),
  netFee: (r) => ({ net: r.net }),
};

const readBody = (request) => new Promise((resolve) => {
  let m = ""; request.on("data", (p) => (m += p)); request.on("end", () => resolve(m));
});

const serve = (port, name, table) => createServer(async (request, response) => {
  const path = new URL(request.url, "http://local").pathname;
  let body;
  if (path === "/rpc") {
    const { procedure, no } = JSON.parse(await readBody(request));
    body = PROCEDURE[procedure]?.(table[no]) ?? { error: "unknown_procedure" };
  } else if (path === "/query") {
    const { no, fields } = JSON.parse(await readBody(request));
    body = Object.fromEntries(fields.map((f) => [f, table[no][f]]));
  } else {
    body = table[path.split("/").pop()] ?? { error: "not_found" };
  }
  const text = JSON.stringify(body);
  response.writeHead(200, { "content-type": "application/json" }).end(text);
}).listen(port, "127.0.0.1", () => console.log(`${name} 127.0.0.1:${port}`));

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

The measurement stands in for the gateway: it builds the same tracking response in each style and
collects four numbers. The byte counts count only the body; header and protocol overhead were left
out of K01's calculation too. The name count is the counterpart-side names that appear in the
caller's code — path name, procedure name, and read field name. The service names are the same in
all three styles and come from the previous lesson's discovery layer, so they are not counted.

```js
// communication/measure.mjs — measures the same internal call three ways, then applies K01's peak read rate
const READ = ["state", "zone", "updatedAt", "route", "net"];   // fields the gateway reads from the response
let calls = 0, sent = 0, received = 0;

async function call(port, path, body) {
  calls += 1;
  const text = body === undefined ? undefined : JSON.stringify(body);
  if (text) sent += Buffer.byteLength(text);
  const y = await fetch(`http://127.0.0.1:${port}${path}`, text ? { method: "POST", body: text } : {});
  const reply = await y.text();
  received += Buffer.byteLength(reply);
  return JSON.parse(reply);
}
const reset = () => { calls = sent = received = 0; };

const STYLE = {
  "resource-based": { path: ["/shipment", "/fee"], procedure: [],
    run: async (n) => [await call(8471, `/shipment/${n}`), await call(8472, `/fee/${n}`)] },
  "remote procedure call": { path: ["/rpc"], procedure: ["trackingSummary", "netFee"],
    run: async (n) => [await call(8471, "/rpc", { procedure: "trackingSummary", no: n }),
      await call(8472, "/rpc", { procedure: "netFee", no: n })] },
  "query-based": { path: ["/query"], procedure: [],
    run: async (n) => [await call(8471, "/query", { no: n, fields: READ.slice(0, 4) }),
      await call(8472, "/query", { no: n, fields: ["net"] })] },
};

const MEASURE = ["internal call", "sent bytes", "received bytes", "total internal bytes",
  "names caller knows", "  path name", "  procedure name", "  field name"];
const result = [];
for (const [name, style] of Object.entries(STYLE)) {
  reset();
  const [a, c] = await style.run("TR-4821");
  const read = READ.filter((k) => a[k] !== undefined || c[k] !== undefined).length;
  result.push([name, { "internal call": calls, "sent bytes": sent, "received bytes": received,
    "total internal bytes": sent + received, "  path name": style.path.length, "  procedure name": style.procedure.length,
    "  field name": read, "names caller knows": style.path.length + style.procedure.length + read }]);
}
console.log(`${"measure".padEnd(22)}${result.map(([n]) => n.padStart(22)).join("")}`);
for (const m of MEASURE) console.log(`${m.padEnd(22)}${result.map(([, s]) => String(s[m]).padStart(22)).join("")}`);

// K01 Back-of-the-Envelope Estimation: peak read 416.67 req/s; K02 gateway aggregation preserves this rate
const PEAK_READ = 416.67, V5 = 480, K01_EGRESS = 1.6;
console.log(`\n${"style".padEnd(22)}${"internal req/s".padStart(17)}${"internal boundary Mbit/s".padStart(27)}` +
  `${"ratio to K01 read egress".padStart(27)}`);
for (const [name, s] of result) {
  const mbit = (PEAK_READ * s["total internal bytes"] * 8) / 1e6;
  console.log(`${name.padEnd(22)}${(PEAK_READ * s["internal call"]).toFixed(2).padStart(17)}` +
    `${mbit.toFixed(3).padStart(27)}${(mbit / K01_EGRESS).toFixed(2).padStart(27)}`);
}
const resource = result[0][1]["total internal bytes"], rpc = result[1][1]["total internal bytes"];
console.log(`resource-based / remote procedure call byte ratio = ${(resource / rpc).toFixed(2)};` +
  ` K01's outbound tracking response is ${V5} bytes (V5)`);
```

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

```
delivery-ops  127.0.0.1:8471
billing       127.0.0.1:8472
measure                       resource-based remote procedure call           query-based
internal call                              2                     2                     2
sent bytes                                 0                    83                    95
received bytes                           373                   112                   112
total internal bytes                     373                   195                   207
names caller knows                         7                     8                     6
  path name                                2                     1                     1
  procedure name                           0                     2                     0
  field name                               5                     5                     5

style                    internal req/s   internal boundary Mbit/s   ratio to K01 read egress
resource-based                   833.34                      1.243                       0.78
remote procedure call            833.34                      0.650                       0.41
query-based                      833.34                      0.690                       0.43
resource-based / remote procedure call byte ratio = 1.91; K01's outbound tracking response is 480 bytes (V5)
```

## The Number That Does Not Change

The table's first row is 2 in all three columns. This is the lesson's most important result: **the
call style does not change the internal call count.** Two calls are made not because of the
protocol, but because the data sits in two separate contexts — the state in delivery operations,
the amount in billing. Choosing a style neither builds this split nor removes it.

The number's importance surfaces in the next lesson: the number of steps in the chain is the
number of pieces the latency budget divides into, and that number is set here, by the domain
split. It cannot be shrunk by choosing a style.

## Bytes and Names

The second group of rows sets two measures against each other, and they move in opposite
directions.

**Bytes.** The resource-based style carries 373 bytes, remote procedure call 195, query-based 207.
The resource-based style brings back two complete records: the carrier, weight, volume, origin
branch, and destination branch also cross the wire, because the address corresponds to a
**record**. In the remote procedure call style, the server has fixed the response to the call's
purpose; the query-based style brings back the same fields, but because the requested field names
are also written into the request, the sent bytes climb to 95. Ratio 1.91: nearly twice the bytes
for the same job.

**Names.** The number of names the caller has to know is 6 in the query-based style, 7 in
resource-based, 8 in remote procedure call. The ranking does not match the byte ranking. The remote
procedure call style carries the fewest bytes but requires knowing the most names, because it adds
a layer of procedure names on top of the field names: `trackingSummary` and `netFee` are two names
written into the caller's code, and both of them can change.

The name count is a coupling measure and is the direct input to the next lesson: a name changing
means every consumer that reads that name changes. But the names' **owner** also has to be
counted. The resource-based style's five field names belong to the record itself and are shared by
every consumer; the `trackingSummary` procedure is written for a single caller. The same count of
names does not mean the same difficulty of change.

## Back to the Calculation

The table below ties the measured bytes to K01's rate. The inputs: peak read 416.67 req/s (K01)
and the gateway's aggregation decision, which did not change the external request rate (K02).

The internal request rate is 833.34 req/s in all three styles — twice the external rate, because
every external request spawns two internal calls. This number was not in K01's table; K01 counted
the rate at the edge, not the application's own internal calls. This is the first consequence of
splitting the service on the system: **the internal request rate is a number independent of the
external request rate, and it is multiplied by the split.**

The internal boundary bandwidth is 1.243 Mbit/s in the resource-based style, 0.650 in remote
procedure call, 0.690 in query-based. The comparison point is K01's read egress: 1.60 Mbit/s. So
the traffic crossing the internal boundary, even in the most expensive style, stays at 0.78x the
outbound traffic. The number says two things. First, the bandwidth between the two services is not
a constraint at this scale; the style choice cannot be made by looking at bytes alone. Second, the
ratio grows with scale — once the split deepens and every external request spawns four or six
internal calls, internal traffic overtakes external traffic, and at that point the same table
dictates a different decision.

## Summary

- The three styles were compared at the client boundary in the Web API Design course; at the
  service boundary both ends are ours, so the measures change: internal call count, internal
  bytes, and the number of names the caller knows.
- The internal call count came out to 2 in all three styles: what sets the call count is not the
  protocol, it is the data sitting in two separate contexts.
- Internal bytes are 373 in the resource-based style, 195 in remote procedure call, 207 in
  query-based; the ratio is 1.91, and the source of the difference is that the address corresponds
  to a record.
- The number of names the caller knows is 6 in query-based, 7 in resource-based, 8 in remote
  procedure call; remote procedure call carries the fewest bytes while requiring the most names,
  because it adds a layer of procedure names on top of the field names.
- The owner of the names matters as much as their count: record fields are shared with every
  consumer, a purpose-built procedure name belongs to a single caller.
- Back to K01: the internal request rate is 833.34 req/s (twice the external rate) and the internal
  boundary is 0.650–1.243 Mbit/s, that is, 0.41–0.78x K01's 1.60 Mbit/s read egress; the ratio
  grows as the split deepens.

## Next Step

This lesson counted the names the caller has to know and showed the count moves between six and
eight. The number alone says nothing; its meaning surfaces once one of those names changes. The
gateway is not the only caller: the same two services are also called by the end-of-day billing
job, the interface built for the front end, and the reporting stream, and each of them reads a
different subset of the domain. The next lesson counts how many consumers break when one domain
name changes, shows that the same change can be split into a sequence to bring the number of broken
consumers to zero, and computes what that costs the internal boundary during the transition window.
