---
title: Microservices
source: 'https://academia.sh/en/courses/service-architectures/microservices'
course: 'Service Architectures'
language: en
updated: '2026-08-23T07:00:29+00:00'
license: 'CC BY-SA 4.0'
---

# Microservices

Splitting the same loan workflow into genuinely separate processes: five processes, eight endpoints, nine network hops per request, chained latency; one deployment unit redeploying without stopping the others, and the unfinished workflow born when one process goes down.

The previous lesson split the units apart but kept them in the same process: `send` was a
function call, a message was an object. This lesson splits the same loan workflow into
**genuinely separate processes**. Each unit listens on its own port, holds its own data,
starts on its own, and stops on its own. This arrangement, where each unit has its own
deployment unit, is called a **microservice**; the definition of the style was covered in the
Architectural Styles course, what is measured here is the cost the split produces in the run
and in the code.

**AO5.** Five processes talk to each other on the same machine, over the loopback interface.
The latency, packet loss, and fragmentation of a real network do not exist in this
arrangement. The properties the model carries are: a call **crosses a process boundary**, it
gets serialized, the target process can go down independently and can be restarted
independently.

## Five Processes

Each service takes its port from the command line and reads its neighbors' addresses from an
environment variable. State-changing endpoints are called with `POST`.

```js
// catalog-service.mjs — book unit; its own process, its own data
import { createServer } from "node:http";
const port = Number(process.argv[2]);
if (!port) { console.log("usage: node catalog-service.mjs <port>"); process.exit(0); }
const book = { 1: { status: "on_shelf" }, 2: { status: "on_shelf" }, 3: { status: "on_shelf" } };
createServer((request, response) => {
  const u = new URL(request.url, "http://y");
  const no = u.searchParams.get("bookNo");
  if (u.pathname === "/status") return response.end(JSON.stringify({ status: book[no].status }));
  if (u.pathname === "/mark-on-loan") { book[no].status = "on_loan"; return response.end("{}"); }
  response.statusCode = 404;
  response.end("{}");
}).listen(port, () => console.log(`catalog ${port}`));
```

```js
// membership-service.mjs — member unit; its own process, its own data
import { createServer } from "node:http";
const port = Number(process.argv[2]);
if (!port) { console.log("usage: node membership-service.mjs <port>"); process.exit(0); }
const member = { 4: { name: "Derek", openLoans: 1, limit: 5, balance: 0 },
              5: { name: "Grace", openLoans: 3, limit: 3, balance: 0 } };
createServer((request, response) => {
  const u = new URL(request.url, "http://y");
  const m = member[u.searchParams.get("memberNo")];
  if (m && u.pathname === "/get") return response.end(JSON.stringify(m));
  if (m && u.pathname === "/add-open-loan") { m.openLoans += 1; return response.end("{}"); }
  if (m && u.pathname === "/add-balance") {
    m.balance += Number(u.searchParams.get("amount"));
    return response.end("{}");
  }
  response.statusCode = 404;
  response.end("{}");
}).listen(port, () => console.log(`membership ${port}`));
```

```js
// fee-service.mjs — late fee unit; daily rate lives here
import { createServer } from "node:http";
const port = Number(process.argv[2]);
if (!port) { console.log("usage: node fee-service.mjs <port>"); process.exit(0); }
const DAILY_RATE = 2;
createServer((request, response) => {
  const u = new URL(request.url, "http://y");
  if (u.pathname !== "/calculate") { response.statusCode = 404; return response.end("{}"); }
  const days = Number(u.searchParams.get("days"));
  response.end(JSON.stringify({ fee: days > 0 ? days * DAILY_RATE : 0 }));
}).listen(port, () => console.log(`fee ${port}`));
```

```js
// notification-service.mjs — notification unit; asks the membership process for the member's name
import { createServer } from "node:http";
const port = Number(process.argv[2]);
if (!port) { console.log("usage: node notification-service.mjs <port>"); process.exit(0); }
createServer(async (request, response) => {
  const u = new URL(request.url, "http://y");
  if (u.pathname !== "/send") { response.statusCode = 404; return response.end("{}"); }
  const y = await fetch(`http://127.0.0.1:${process.env.MEMBERSHIP}/get?memberNo=${u.searchParams.get("memberNo")}`);
  console.log(`notification -> ${(await y.json()).name}: ${u.searchParams.get("text")}`);
  response.end("{}");
}).listen(port, () => console.log(`notification ${port}`));
```

The loan service runs the same sequence as `loan.mjs` in the third lesson. The only
difference is that each step is now a network call. It counts the hops it makes and writes
the duration of the chain to its own log.

```js
// loan-service.mjs — the process running the workflow; calls the others over the network
import { createServer } from "node:http";
const port = Number(process.argv[2]);
if (!port) { console.log("usage: node loan-service.mjs <port>"); process.exit(0); }
const A = { catalog: process.env.CATALOG, membership: process.env.MEMBERSHIP,
            fee: process.env.FEE, notification: process.env.NOTIFICATION };
let hop = 0;
const call = async (target, path, method = "GET") => {
  hop += 1;
  return (await fetch(`http://127.0.0.1:${A[target]}${path}`, { method })).json();
};
createServer(async (request, response) => {
  const u = new URL(request.url, "http://y");
  if (u.pathname !== "/loan") { response.statusCode = 404; return response.end("{}"); }
  const memberNo = u.searchParams.get("memberNo"), bookNo = u.searchParams.get("bookNo");
  const days = u.searchParams.get("days") ?? 0;
  hop = 0;
  const t0 = performance.now();
  try {
    const member = await call("membership", `/get?memberNo=${memberNo}`);
    if (member.openLoans >= member.limit) throw new Error("loan limit exceeded");
    if ((await call("catalog", `/status?bookNo=${bookNo}`)).status !== "on_shelf")
      throw new Error("book not on shelf");
    await call("catalog", `/mark-on-loan?bookNo=${bookNo}`, "POST");
    await call("membership", `/add-open-loan?memberNo=${memberNo}`, "POST");
    const { fee } = await call("fee", `/calculate?days=${days}`);
    if (fee > 0) await call("membership", `/add-balance?memberNo=${memberNo}&amount=${fee}`, "POST");
    await call("notification", `/send?memberNo=${memberNo}&text=book+${bookNo}`, "POST");
    console.log(`loan ${bookNo}/${memberNo}: hop ${hop}, duration ${Math.round(performance.now() - t0)} ms`);
    response.end(JSON.stringify({ status: "issued", fee, hop }));
  } catch (h) {
    console.log(`loan ${bookNo}/${memberNo}: ERROR ${h.message}, hop ${hop}`);
    response.statusCode = 400;
    response.end(JSON.stringify({ status: "failed", error: h.message, hop }));
  }
}).listen(port, () => console.log(`loan ${port}`));
```

## Bringing It Up

```sh
# start.sh — five processes come up; each process's output is written to its own log
rm -f pid.txt
node catalog-service.mjs 8792 >catalog.log 2>&1 & echo "catalog $!" >>pid.txt
node membership-service.mjs 8793 >membership.log 2>&1 & echo "membership $!" >>pid.txt
node fee-service.mjs 8794 >fee.log 2>&1 & echo "fee $!" >>pid.txt
MEMBERSHIP=8793 node notification-service.mjs 8795 >notification.log 2>&1 & echo "notification $!" >>pid.txt
CATALOG=8792 MEMBERSHIP=8793 FEE=8794 NOTIFICATION=8795 \
  node loan-service.mjs 8791 >loan.log 2>&1 & echo "loan $!" >>pid.txt
for p in 8791 8792 8793 8794 8795; do
  for i in $(seq 40); do curl -s -o /dev/null "http://127.0.0.1:$p/" && break; sleep 0.2; done
done
echo "process brought up: $(wc -l <pid.txt | tr -d ' ')   startup step: 5 background + 1 readiness wait"
echo "endpoint: $(grep -oh '"/[a-z-]*"' ./*.mjs | sort -u | wc -l | tr -d ' ')   deployment unit: 5"
grep -oh '"/[a-z-]*"' ./*.mjs | sort -u | tr -d '"' | tr '\n' ' '
echo
```

```
process brought up: 5   startup step: 5 background + 1 readiness wait
endpoint: 8   deployment unit: 5
/add-balance /add-open-loan /calculate /get /loan /mark-on-loan /send /status
```

In the first lesson this number was one: `node app.mjs`. Here there are five background
processes and one readiness wait; ports and neighbor addresses have to be supplied from
outside.

## Hops Per Request

```sh
curl -s "http://127.0.0.1:8791/loan?bookNo=1&memberNo=4&days=3"
echo
curl -s "http://127.0.0.1:8791/loan?bookNo=2&memberNo=5"
echo
```

```
{"status":"issued","fee":6,"hop":7}
{"status":"failed","error":"loan limit exceeded","hop":1}
```

The loan service made 7 calls. The notification service made one more call to get the
member's name; together with the request the client sent to the loan service, that comes to
**9 network hops per request** in total. In the third lesson the same workflow produced 8
messages — the message count did not change, every message just came to cross a process
boundary.

The chain proceeds in sequence: each call waits for the previous one's return, so the
latencies add up. The loan service writes one line to `loan.log` for every request; the first
request's line came out as `loan 1/4: hop 7, duration 21 ms` in this run.
The duration field depends on the machine, the core, and the load at that moment — the value
above was read in this run and changes on every run. The quantity that does not depend on the
run is **7 sequential round trips**, and this number only drops if the chain is shortened.
The second request stopped at the membership check, so it finished with a single hop: when an
error surfaces early, the rest of the chain never gets built.

## Independent Deployment

The real payoff is measured here: let the fee rate change.

```sh
# restart.sh — only the fee process changes and restarts; the others never stop
kill "$(awk '$1 == "fee" { print $2 }' pid.txt)"
sed -i.bak 's/const DAILY_RATE = 2;/const DAILY_RATE = 5;/' fee-service.mjs
rm -f fee-service.mjs.bak
node fee-service.mjs 8794 >>fee.log 2>&1 & echo "fee $!" >>pid.txt
for i in $(seq 40); do curl -s -o /dev/null "http://127.0.0.1:8794/" && break; sleep 0.2; done
echo "process restarted: 1 / 5   deployment unit touched: 1"
curl -s "http://127.0.0.1:8791/loan?bookNo=2&memberNo=4&days=3"
echo
```

```
process restarted: 1 / 5   deployment unit touched: 1
{"status":"issued","fee":15,"hop":7}
```

The new rate is in effect. The catalog, membership, notification, and loan processes never
stopped; they kept their in-memory state. In the first lesson the same one-line change
redeployed seven modules together; here, one deployment unit restarted.

This payoff is not unlimited. The canonical `openLoans` field appears in two deployment
units — membership and loan. Renaming it was a change touching 3 files within a single unit
in the third lesson; here it requires **two separate deployment units to release in a
coordinated order**. Independent deployment is independent only for changes that do not touch
the contract.

## Partial Failure

```sh
# fault.sh — the notification process stops; the same request breaks at the end of its path
kill "$(awk '$1 == "notification" { print $2 }' pid.txt)"
sleep 0.5
curl -s "http://127.0.0.1:8791/loan?bookNo=3&memberNo=4"
echo
curl -s "http://127.0.0.1:8792/status?bookNo=3"
echo
curl -s "http://127.0.0.1:8793/get?memberNo=4"
echo
```

```
{"status":"failed","error":"fetch failed","hop":6}
{"status":"on_loan"}
{"name":"Derek","openLoans":4,"limit":5,"balance":21}
```

The request came back failed, but the book is on loan and the member's open loan count went
up. The chain that broke on the sixth hop did not undo the effect of the first five hops. This
did not happen in the monolith: an exception left the whole body, and no module was left
having finalized half a job.

The second line of the same output says something else: the catalog service is still
responding. When the notification process went down, the system as a whole did not stop —
paths that do not touch notification keep working. The failure is now **partial**; this is
both a gain and a new problem, because the state of the system now sits somewhere between
"up" and "down".

```sh
# stop.sh — stops every process
while read -r name p; do kill "$p" 2>/dev/null; done <pid.txt
echo "stopped"
```

```
stopped
```

## Three Columns

| | Microservices |
|---|---|
| Cheapens | A rate change restarted 1 of 5 deployment units; each unit owns its own data, the scaling unit is a single service |
| Makes expensive | 9 network hops and 7 sequential round trips per request; startup went from 1 step to 6 steps; addresses have to be supplied from outside |
| Failure mode created | A request that breaks mid-chain leaves an unfinished state behind; the failure is partial, the system is neither fully up nor fully down |

## Summary

- The same loan workflow was split into 5 separate processes: 8 endpoints, 5 deployment
  units, 9 network hops per request, 7 sequential round trips.
- The message count did not change from the third lesson; what changed is that every message
  now crosses a process boundary, and sequential round trips add up their latencies.
- Bringing the system up went from 1 step to 6; every process's port and neighbor addresses
  have to be supplied from outside.
- A fee rate change restarted only 1 deployment unit; the other four processes never stopped.
  A change that touches the contract forces two units to release in a coordinated order.
- When the notification process went down, the request came back failed, but the book was on
  loan and the member's counter had already gone up: a workflow broken mid-chain leaves an
  unfinished state behind.

## Next Step

These five processes are long-lived: they stay up whether a request arrives or not, keep
their state in memory, and are started once and left running. The next lesson shrinks the
deployment unit by one more step and removes long-livedness: the unit becomes a function that
exists only when an event triggers it. What gets measured are the extra calls that come from
being unable to hold anything in memory between two calls, a per-call time limit splitting the
workflow, and the last local calls still left inside a process turning into network calls too.
