---
title: 'Shared Kernel and Conformist'
source: 'https://academia.sh/en/courses/domain-driven-design/shared-kernel-and-conformist'
course: 'Domain-Driven Design'
language: en
updated: '2026-08-23T07:01:18+00:00'
license: 'CC BY-SA 4.0'
---

# Shared Kernel and Conformist

Two collaboration patterns: counting the rate of changes requiring a joint decision and the number of files two teams edit together as a shared kernel grows, measuring the core files used by a single context, and comparing the conformist arrangement's file and line savings against the number of names left in downstream's own vocabulary.

The previous two lessons rested on the same assumption: the other side is a stranger, and a
translation stands in between. That assumption does not hold in two cases. First, when two
contexts are held by the same team, or by two teams that plan together; maintaining a
translation between them means making the same decision in two places. Second, when the
downstream team has no power at all to influence the upstream; whether it writes a
translation or not, the upstream changes however it wants.

The answer to the first case is a part held in common: a **shared kernel**. The answer to the
second is giving up translation: being a **conformist**. Both are decisions whose cost can be
measured. This lesson counts how many files two teams are forced to edit together as the
kernel grows, then compares the conformist's gain against the number of names it leaves in
downstream's own vocabulary.

## Defining the Shared Kernel

A shared kernel is a piece of the model that two contexts jointly own. Joint ownership
requires more than sharing code: a change inside the kernel needs both teams' approval, both
sides' tests run together, releases are planned together. That is why the kernel is a
workable arrangement when kept small, and why it collapses the two contexts back into one
when it grows.

The measure is straightforward: whether a change touches the kernel is whether it requires a
joint decision. The change log below carries sixteen model changes from the shipment pricing
and routing library, each with the domain concept it touches.

```js
// change-log.mjs — sixteen changes and the domain concept each one touches
export const LOG = [
  ["2025-01-14", "unit-of-measure"], ["2025-01-28", "tariff-tier"],
  ["2025-02-11", "state-set"], ["2025-02-25", "tariff-tier"],
  ["2025-03-11", "chargeable-weight-rule"], ["2025-03-25", "discount-rule"],
  ["2025-04-08", "tariff-tier"], ["2025-04-22", "state-set"],
  ["2025-05-06", "vehicle-selection"], ["2025-05-20", "unit-of-measure"],
  ["2025-06-03", "tariff-tier"], ["2025-06-17", "chargeable-weight-rule"],
  ["2025-07-01", "discount-rule"], ["2025-07-15", "state-set"],
  ["2025-07-29", "contract-threshold"], ["2025-08-12", "transfer-rule"],
];
```

The same eight concepts land in different files across two arrangements. In the small
kernel, the only thing held in common is the measure: the definition of the gram and the
cubic decimeter, and the rule for converting volumetric weight into chargeable weight. In the
large kernel, the tariff tier and the state set are also pulled into the kernel — even though
both are a single context's concept.

```js
// shared-count.mjs — how many changes and files a shared kernel forces two teams to edit together as it grows
import { LOG } from "./change-log.mjs";

const SMALL = {
  "unit-of-measure": ["core/unit.mjs", ["pricing", "delivery"]],
  "chargeable-weight-rule": ["core/unit.mjs", ["pricing", "delivery"]],
  "tariff-tier": ["pricing/tariff.mjs", ["pricing"]],
  "discount-rule": ["pricing/discount.mjs", ["pricing"]],
  "contract-threshold": ["pricing/discount.mjs", ["pricing"]],
  "state-set": ["delivery/state.mjs", ["delivery"]],
  "vehicle-selection": ["delivery/vehicle.mjs", ["delivery"]],
  "transfer-rule": ["delivery/route.mjs", ["delivery"]],
};

const LARGE = {
  ...SMALL,
  "tariff-tier": ["core/tariff.mjs", ["pricing"]],
  "state-set": ["core/state.mjs", ["delivery"]],
};

for (const [name, mapping] of [["small kernel", SMALL], ["large kernel", LARGE]]) {
  const core = new Map();
  for (const [file, users] of Object.values(mapping)) {
    if (file.startsWith("core/")) core.set(file, users);
  }
  const sharedChanges = LOG.filter(([, k]) => mapping[k][0].startsWith("core/"));
  const touched = new Set(sharedChanges.map(([, k]) => mapping[k][0]));
  const singleUseCore = [...core].filter(([, users]) => users.length === 1);
  console.log(`${name}: ${core.size} core file(s)`);
  for (const [file, users] of [...core].sort()) {
    console.log(`  ${file.padEnd(20)} used by context: ${users.join(", ")}`);
  }
  console.log(`  changes requiring joint decision = ${sharedChanges.length} / ${LOG.length}`);
  console.log(`  files the two teams edit together = ${touched.size}`);
  console.log(`  core files used by a single context = ${singleUseCore.length} / ${core.size}`);
  for (const b of ["pricing", "delivery"]) {
    const independent = LOG.filter(([, k]) => !mapping[k][0].startsWith("core/") && mapping[k][1].includes(b));
    console.log(`  ${b.padEnd(14)} changes it can make alone = ${independent.length}`);
  }
}
```

```sh
node shared-count.mjs
```

```
small kernel: 1 core file(s)
  core/unit.mjs        used by context: pricing, delivery
  changes requiring joint decision = 4 / 16
  files the two teams edit together = 1
  core files used by a single context = 0 / 1
  pricing        changes it can make alone = 7
  delivery       changes it can make alone = 5
large kernel: 3 core file(s)
  core/state.mjs       used by context: delivery
  core/tariff.mjs      used by context: pricing
  core/unit.mjs        used by context: pricing, delivery
  changes requiring joint decision = 11 / 16
  files the two teams edit together = 3
  core files used by a single context = 2 / 3
  pricing        changes it can make alone = 3
  delivery       changes it can make alone = 2
```

In the small kernel, four of sixteen changes require a joint decision; twelve are changes a
team can make alone. Once the kernel grows from one file to three, the changes requiring a
joint decision rise from four to eleven, and the changes the two teams can make alone drop
from twelve to five. The joint-ownership rate has risen from 25 percent to 69 percent.

One of the last lines shows the decision's wrong side: two of the three files in the large
kernel are used by only a single context. `core/tariff.mjs` is pricing's concept alone,
`core/state.mjs` is delivery's concept alone; both sit under joint ownership and both wait on
approval from a team that does not own them. The kernel's criterion follows from this: a file
belongs in the kernel only if both contexts genuinely use it.

## What Belongs in the Kernel

The two concepts in the small kernel are not there by accident. The unit of measure and the
rule converting volumetric weight into chargeable weight say the same thing in both contexts:
pricing uses it to compute a fee, delivery uses it to select a vehicle, but the definition is
single and both sides must arrive at the same number. Keeping two definitions here is not a
gain; it is a source of inconsistency.

The tariff tier and the state set are not like that. Where the tiers fall is pricing's
decision; delivery has no need to know it. Which values the state set carries is delivery's
decision; for pricing, a shipment is either priced or it is not. Pulling these two concepts
into the kernel is not a modeling decision — it is the convenience of leaving a file in the
middle.

The criterion can be written in one sentence: only a concept that both contexts use with the
same meaning, and that would produce an inconsistency if defined separately, belongs in the
kernel.

## Conformist

The second pattern comes from an entirely different situation. The downstream team has no
power to change the upstream's model; the upstream makes its own decisions on its own, and
downstream's needs do not enter its plan. In this situation, downstream has two options:
write a translation, or adopt the upstream's model as it is. The second is the **conformist**
pattern.

The upstream is the pricing context, and it gives the tariff line under its own names.

```sh
mkdir -p upstream conformist translated
```

```js
// upstream/tariff.mjs — upstream: the pricing context's tariff model
const TIERS = [
  { maxChargeableGrams: 1000, tierFeeCents: 4990 },
  { maxChargeableGrams: 5000, tierFeeCents: 8490 },
  { maxChargeableGrams: 30000, tierFeeCents: 24990 },
];
const ZONE_FACTOR = { near: 1, mid: 1.35, far: 1.8 };

export function tariffLine(chargeableGrams, feeZone) {
  const t = TIERS.find((x) => chargeableGrams <= x.maxChargeableGrams);
  return t === undefined ? null
    : { tierFeeCents: t.tierFeeCents, zoneFactor: ZONE_FACTOR[feeZone] ?? 1.8 };
}
```

The downstream is the delivery context, and it needs the shipment's transport value to
select a vehicle. In the conformist arrangement, it writes a single file and uses the
upstream's names directly.

```js
// conformist/vehicle.mjs — downstream adopts the upstream's model as it is
import { tariffLine } from "../upstream/tariff.mjs";

export function selectVehicle(chargeableGrams, feeZone) {
  const t = tariffLine(chargeableGrams, feeZone);
  if (t === null) return "hand-delivery";
  const amount = t.tierFeeCents * t.zoneFactor;
  if (amount >= 30000) return "box-truck";
  return chargeableGrams > 5000 ? "van" : "motorcycle";
}
```

In the translated arrangement, the same work is split across two files; the boundary file
translates the upstream's names into delivery's language.

```js
// translated/transport-translation.mjs — translates the upstream names into transport's own language
import { tariffLine } from "../upstream/tariff.mjs";

export function transportTerms(chargeableGrams, feeZone) {
  const t = tariffLine(chargeableGrams, feeZone);
  if (t === null) return { isPriceable: false, valueCents: 0, loadGrams: chargeableGrams };
  return { isPriceable: true, valueCents: t.tierFeeCents * t.zoneFactor, loadGrams: chargeableGrams };
}
```

```js
// translated/vehicle.mjs — downstream core: speaks only its own language
import { transportTerms } from "./transport-translation.mjs";

export function selectVehicle(loadGrams, zone) {
  const k = transportTerms(loadGrams, zone);
  if (!k.isPriceable) return "hand-delivery";
  if (k.valueCents >= 30000) return "box-truck";
  return k.loadGrams > 5000 ? "van" : "motorcycle";
}
```

## The Gain and Cost of Conformity

The measure consists of three numbers: the file and line count, whether the upstream's names
appear in downstream's core, and how many names remain in downstream's own vocabulary.

```js
// conformity-count.mjs — file, line, and upstream-name count for the conformist vs. translated arrangement
import { readFileSync } from "node:fs";
import { selectVehicle as conformistSelect } from "./conformist/vehicle.mjs";
import { selectVehicle as translatedSelect } from "./translated/vehicle.mjs";

const UPSTREAM_NAMES = ["tariffLine", "tierFeeCents", "zoneFactor", "feeZone", "chargeableGrams"];
const DOWNSTREAM_NAMES = ["isPriceable", "valueCents", "loadGrams", "zone"];
const present = (text, names) => names.filter((a) => new RegExp(`\\b${a}\\b`).test(text));

for (const [name, files] of [
  ["conformist", ["conformist/vehicle.mjs"]],
  ["translated", ["translated/transport-translation.mjs", "translated/vehicle.mjs"]],
]) {
  const lines = files.reduce((t, d) => t + readFileSync(d, "utf8").trimEnd().split("\n").length, 0);
  console.log(`${name}: ${files.length} file(s), ${lines} lines`);
  let core = 0, leakingCore = 0;
  for (const d of files) {
    const text = readFileSync(d, "utf8");
    const up = present(text, UPSTREAM_NAMES), own = present(text, DOWNSTREAM_NAMES);
    const boundary = d.includes("-translation.mjs");
    if (!boundary) { core += 1; if (up.length > 0) leakingCore += 1; }
    console.log(`  ${d.padEnd(40)} upstream name ${up.length}, own name ${own.length}`);
  }
  console.log(`  core files carrying an upstream name = ${leakingCore} / ${core}`);
}

const EXAMPLES = [[800, "mid"], [4000, "far"], [20000, "near"], [20000, "far"], [50000, "mid"]];
let differing = 0;
for (const [grams, zone] of EXAMPLES) {
  const a = conformistSelect(grams, zone), b = translatedSelect(grams, zone);
  if (a !== b) differing += 1;
  console.log(`${String(grams).padStart(5)} g  ${zone.padEnd(5)} conformist ${a.padEnd(13)} translated ${b}`);
}
console.log(`differing vehicle = ${differing} / ${EXAMPLES.length}`);
```

```sh
node conformity-count.mjs
```

```
conformist: 1 file(s), 10 lines
  conformist/vehicle.mjs                   upstream name 5, own name 0
  core files carrying an upstream name = 1 / 1
translated: 2 file(s), 17 lines
  translated/transport-translation.mjs     upstream name 5, own name 3
  translated/vehicle.mjs                   upstream name 0, own name 4
  core files carrying an upstream name = 0 / 1
  800 g  mid   conformist motorcycle    translated motorcycle
 4000 g  far   conformist motorcycle    translated motorcycle
20000 g  near  conformist van           translated van
20000 g  far   conformist box-truck     translated box-truck
50000 g  mid   conformist hand-delivery translated hand-delivery
differing vehicle = 0 / 5
```

The gain is concrete: one file and seven lines fewer. The cost is concrete too. In the
conformist arrangement, downstream's one core file carries five of the upstream's names and
carries none of its own vocabulary (own name 0). In the translated arrangement, the core file
carries not a single name from upstream and carries four of its own. The vehicle selected in
all five examples is the same in both arrangements.

This is not enough to read the conformist pattern as a mistake. If downstream genuinely finds
the upstream's model fitting, a translation is just a file mapping two names to each other,
and its cost buys nothing back. The pattern is written as a choice: downstream gives up the
right to build its own model in exchange for contract conformity. What is wrong is making
that trade without realizing it — then downstream's own vocabulary never gets built, and the
business-rule file gets edited every time the upstream changes.

## What the Two Patterns Share

A shared kernel and a conformist give up the same thing: translation. The difference is what
is received in exchange for giving it up. In a shared kernel, both sides give up part of
their decision rights and receive a single definition in return; its measure is the rate of
changes requiring a joint decision. In a conformist arrangement, only one side gives
something up, receiving files and lines in return; its measure is the number of names left in
downstream's own vocabulary. Both are bought at a cost, and both are countable.

## Summary

- A shared kernel is a piece of the model that two contexts jointly own; every change
  touching the kernel requires both teams' decision.
- Across sixteen changes, once the kernel grew from one file to three, changes requiring a
  joint decision rose from 4/16 to 11/16; changes the teams could make alone dropped from 12
  to 5.
- Two of the three files in the large kernel were used by only a single context; only a
  concept that both contexts use with the same meaning, and that would produce an
  inconsistency if defined separately, belongs in the kernel.
- Conformist means downstream gives up translation and adopts the upstream's model as it is;
  its gain came to 1 file and 7 lines.
- Its cost showed up on the same scale: in the conformist arrangement, downstream's core
  carried 5 names from upstream and 0 of its own; in the translated arrangement, those
  numbers were 0 and 4. The vehicle selected in all five examples was the same in both
  arrangements.
- Both patterns give up translation; a shared kernel receives a single definition in return,
  a conformist receives files and lines in return.

## Next Step

Up to this point, the boundaries of contexts have been drawn, the relationships between them
named, and the cost of three collaboration forms counted. All of these decisions left one
question unanswered: how much design effort should go to which context? Is the
unit-of-measure definition modeled with the same care as the fee rule; does the carrier
provider's state translation belong in the same class as the discount rule? The domain is not
equal within itself: part of it is the work the organization wins by, part keeps that work
standing, and part is the same in every organization. The next lesson builds this three-way
distinction, extracts how much each part actually changed from the change log, and compares
the distribution of effort against the distribution of change to count the effort spent in
the wrong place.
