---
title: 'Microservice Architecture'
source: 'https://academia.sh/en/courses/architectural-styles/microservice-architecture'
course: 'Architectural Styles'
language: en
updated: '2026-08-23T07:01:09+00:00'
license: 'CC BY-SA 4.0'
---

# Microservice Architecture

What is gained and what is paid when the shared contract is removed: running two contexts as separate local processes, bringing the files republished per deployment unit down to 1, counting the boilerplate lines paid for independence, and measuring the number of endpoints that keep responding, and the partial response, when a process is stopped.

In the service-oriented arrangement, the source of the release coupling was the shared
contract: the ten-field canonical record tied even a service that did not read those fields to
a release. The next step is to remove the common format. Each context takes its own contract,
its own data format, and its own release schedule; the call goes directly to the other service,
not through a shared surface.

**Microservice architecture** is this arrangement: every service is a deployment unit that can
be published on its own, with its own process, its own data, and its own contract. Where it
departs from service-oriented architecture is not size but the absence of shared assets — there
is no enterprise-wide canonical model, no common call surface. The measurement in this lesson
runs the two contexts as genuinely separate local processes.

## Three Processes

The pricing context is its own server. Its contract belongs to it alone: it reads only the
fields it needs from the query string and returns its own response format.

```js
// micro/fee.mjs — pricing context: its own process, its own contract, its own release schedule
import { createServer } from "node:http";

const TARIFF = { tier: [[1, 3000], [5, 4800], [20, 9600]], zone: { "34": 100, "06": 115, "35": 125 }, minimum: 2500 };

function price(s) {
  const weight = Number(s.get("weight")), rate = Number(s.get("rate") ?? 0);
  const tier = TARIFF.tier.find(([k]) => weight <= k) ?? [0, 9600];
  const base = Math.max(TARIFF.minimum, Math.round((tier[1] * TARIFF.zone[s.get("zone")]) / 100));
  return { id: s.get("id"), net: base - Math.round(base * Math.min(rate, 0.4)) };
}

const port = Number(process.argv[2]);
if (Number.isInteger(port) === false) console.log("usage: node micro/fee.mjs <port>");
else createServer((request, response) => {
  const body = JSON.stringify(price(new URL(request.url, "http://local").searchParams));
  response.writeHead(200, { "content-type": "application/json" });
  response.end(body);
}).listen(port);
```

```js
// micro/operation.mjs — delivery operation context: separate process, separate contract
import { createServer } from "node:http";

const TREE = { "34": ["34"], "06": ["34", "06"], "35": ["34", "41", "35"] };

function plan(s) {
  const route = TREE[s.get("zone")] ?? ["34"];
  return { id: s.get("id"), route, day: route.length, carrier: route.length > 2 ? "MT" : "AN" };
}

const port = Number(process.argv[2]);
if (Number.isInteger(port) === false) console.log("usage: node micro/operation.mjs <port>");
else createServer((request, response) => {
  const body = JSON.stringify(plan(new URL(request.url, "http://local").searchParams));
  response.writeHead(200, { "content-type": "application/json" });
  response.end(body);
}).listen(port);
```

The offer request is no longer a function call; it is the aggregation of two network calls.
Because calls can fail, the aggregator has to make a decision: reject the request outright, or
return what it got and report what is missing. The implementation below chooses the second and
writes the missing-part count into the response contract.

```js
// micro/offer.mjs — aggregator: calls both services over the network, flags a partial response
import { createServer } from "node:http";

async function get(address, query) {
  const y = await fetch(`${address}?${query}`);
  if (y.ok === false) throw new Error(`${address} did not return 200`);
  return y.json();
}

async function offer(query, id) {
  const [f, p] = await Promise.allSettled([
    get(process.env.FEE_ADDRESS, query), get(process.env.OPERATION_ADDRESS, query),
  ]);
  return {
    id, net: f.status === "fulfilled" ? f.value.net : null,
    day: p.status === "fulfilled" ? p.value.day : null,
    carrier: p.status === "fulfilled" ? p.value.carrier : null,
    missing: [f, p].filter((r) => r.status === "rejected").length,
  };
}

const port = Number(process.argv[2]);
if (Number.isInteger(port) === false) console.log("usage: node micro/offer.mjs <port>");
else createServer(async (request, response) => {
  const s = new URL(request.url, "http://local").searchParams;
  const body = JSON.stringify(await offer(s.toString(), s.get("id")));
  response.writeHead(200, { "content-type": "application/json" });
  response.end(body);
}).listen(port);
```

## Measuring the Two States

The driver script starts three processes, measures all three endpoints, then stops the pricing
process and measures the same three endpoints again. The port numbers depend on the
environment; change them if they are already in use on the machine.

```bash
# measure.sh — three separate processes are started; measured both while all are up and once fee is stopped
node micro/fee.mjs 8791 & FEE=$!
node micro/operation.mjs 8792 & OPERATION=$!
FEE_ADDRESS=http://127.0.0.1:8791/price OPERATION_ADDRESS=http://127.0.0.1:8792/plan \
  node micro/offer.mjs 8793 & OFFER=$!
sleep 1
S="id=G1&weight=4&zone=35&rate=0.15"

measure() {
  for u in 8791/price 8792/plan 8793/offer; do
    printf "  %-12s %s\n" "$u" "$(curl -s -o /dev/null -w '%{http_code} code, %{size_download} bytes' "http://127.0.0.1:$u?$S")"
  done
  printf "  offer body = %s\n" "$(curl -s "http://127.0.0.1:8793/offer?$S")"
}

echo "state 1: three processes up"
measure
kill $FEE
sleep 0.5
echo "state 2: fee process stopped"
measure
kill $OPERATION $OFFER 2>/dev/null

echo "deployment unit = 3"
for d in micro/fee.mjs micro/operation.mjs micro/offer.mjs; do
  printf "  %-20s imports = %s, server boilerplate = %s lines\n" "$d" \
    "$(grep -c '^import' $d)" "$(grep -cE 'createServer|writeHead|response.end|process.argv|Number.isInteger' $d)"
done
```

```
state 1: three processes up
  8791/price   200 code, 22 bytes
  8792/plan    200 code, 59 bytes
  8793/offer   200 code, 57 bytes
  offer body = {"id":"G1","net":5100,"day":3,"carrier":"MT","missing":0}
state 2: fee process stopped
  8791/price   000 code, 0 bytes
  8792/plan    200 code, 59 bytes
  8793/offer   200 code, 57 bytes
  offer body = {"id":"G1","net":null,"day":3,"carrier":"MT","missing":1}
deployment unit = 3
  micro/fee.mjs        imports = 1, server boilerplate = 6 lines
  micro/operation.mjs  imports = 1, server boilerplate = 6 lines
  micro/offer.mjs      imports = 1, server boilerplate = 6 lines
```

## Reading the Numbers

Failure isolation came out at zero in the first lesson: a single defect in one process left
three of six requests unanswered, and the units left standing were 0. Here, when the same
context is stopped, 2 of the three endpoints keep returning 200. The operation endpoint produced
its 59-byte response on its own; it never looked at the pricing side. This is the measured
difference, and it is the style's real justification.

The aggregator endpoint's behavior produces a second number. The offer endpoint returned 200,
but its body changed: the `net` field is `null`, the `missing` field is 1. This **partial
response** is not an accident; it is a design decision the style forces. It was not needed in
the monolithic arrangement, because the call could not fail; here, a field was added to the
response contract, and the client has to know what that field means.

The gain on the release side was measured: every deployment unit's closure is 1 file, so if one
line changes in the pricing rule, the files republished are 1. That number was 4 in the
monolithic arrangement and 3 for the `zone` field in the service-oriented arrangement. The cost
paid shows up in the same output: 6 lines of server boilerplate are repeated in each of the
three files, 18 lines in total. Moving this repetition into a shared file brings the release
coupling back — once that file changes, all three units publish together.

## The Number Behind Distributed Complexity

The style's bill collects in a single place: the outcome of an aggregated request is no longer
one of two states but one of four, depending on the sub-service count. The number of up/down
states grows with $2^n$; for two sub-services there are 4 states, and the measurement actually
ran two of them. In the monolithic arrangement $n = 1$, so the state count was 2: working or not
working.

The growth of the state count is a direct maintainability cost. Every additional state needs a
decision — reject, return a partial response, or use a stale value — and that decision turns
into a contract field, a test scenario, and a client behavior. The measurement tested 3
endpoints and 2 states; if the sub-service count rose to four, the same test suite would have to
face 16 states.

The second cost is the multiplication of contracts. In the service-oriented arrangement, the
number of formats to learn was 1; here, every service publishes its own format. In the
measurement, all three endpoints answered with a different body format: three separate
contracts of 22, 59, and 57 bytes. Removing the canonical model lowered the byte count and broke
the release coupling, but it made the number of formats to learn and track for version
compatibility equal to the service count.

## Summary

- In microservice architecture, every service is a deployment unit that can be published on its
  own, with its own process, its own data, and its own contract; there is no shared canonical
  model and no common call surface.
- When the pricing process was stopped, 2 of the three endpoints kept returning 200; in the
  monolithic arrangement, the same defect had brought the units left standing down to 0.
- The aggregator endpoint gave a partial response: the `net` field is `null`, the `missing`
  field is 1 — the style loaded a new field onto the response contract and a new decision onto
  the client.
- Because every deployment unit's closure is 1 file, the files republished dropped to 1; in
  exchange, 6 lines of server boilerplate were repeated in each of three files, 18 lines in
  total.
- The outcome state of an aggregated request grows with $2^n$: 4 states for two sub-services, 2
  states in the monolithic arrangement.
- The number of body formats to learn was made equal to the service count; in the measurement,
  the three endpoints returned three separate contracts of 22, 59, and 57 bytes.

## Next Step

Across the three styles so far, the direction of the call never changed: the aggregator knows
pricing and operation by name, calls them, and waits for the response. This obligation to know
and this waiting are the common source of every cost measured — the partial response decision
and the contract count both came from here. The next style reverses the direction of the call:
pricing publishes an event, does not know who is listening, and does not wait for a response.
The next lesson measures what changes once the event crosses the system boundary: the number of
consumers the publisher knows about, the number of files edited to add a new consumer, and the
deviation between two consumer designs' results when the same event arrives twice.
