Skip to main content

Legacy Seams and Enabling Points

What This Concept Is

From Michael Feathers:

A seam is a place where you can alter behavior in your program without editing in that place.

Every seam has an enabling point: where you make the decision which behavior is used. Seams let you insert a test double, a probe, or a replacement implementation without editing the code whose behavior you want to swap.

Common seam types:

  • Object seam: pass a collaborator in, rather than newing it inside. Enabling point: the caller.
  • Link seam: replace a linked library at build time. Enabling point: build config.
  • Preprocessor seam (C/C++): #define controls which version compiles. Enabling point: compile flag.
  • Function-pointer / parameter seam (JS example below): pass the function you want to vary as a parameter.
  • Module-level rebinding (TypeScript/JavaScript): export a mutable binding you can reassign from a test.

Why It Matters Here

Tests only see what the code lets them see. Legacy code typically reaches out: new Date(), fetch(...), db.query(...). You cannot characterize or refactor code you cannot isolate. Seams are how you isolate.

Seams are also the handholds of legacy displacement (Cluster 5). You cannot Strangler Fig a module that has no seam between its callers and its internals.

Concrete Example

Before (no seam -- you cannot stub calculateShipping):

async function calculatePrice(order: Order) {
const base = order.items.reduce((a, i) => a + i.price, 0);
const shipping = await calculateShipping(order); // hits network
return base + shipping;
}

After (parameter seam; enabling point is the caller):

async function calculatePrice(
order: Order,
shippingFn: (o: Order) => Promise<number> = calculateShipping
) {
const base = order.items.reduce((a, i) => a + i.price, 0);
const shipping = await shippingFn(order);
return base + shipping;
}

// in a test
const stub = async (_o: Order) => 113;
expect(await calculatePrice(sampleOrder, stub)).toBe(153);

The source code of calculateShipping was not edited. Behavior was altered at the enabling point (the test call site). That is a seam.

Common Confusion / Misconception

"I'll just mock it with a framework." Mocking frameworks often create seams for you, but you still have to know where the seam is. If the class news its dependencies inside its constructor and the framework cannot reach them, no amount of mocking syntax will help -- the seam does not exist. You must introduce one first.

Also: introducing a seam is a refactor. It changes internal structure without changing observable behavior at the original caller (default argument preserves the original call site).

How To Use It

Default rule: the smallest seam wins. A parameter with a default is less invasive than a dependency-injection container.

Check Yourself

  1. Given code that calls Date.now() inside a function, name two seams you could introduce.
  2. What is the difference between a seam and a mock?
  3. Why must every seam have an enabling point? Give a case where the seam exists but the enabling point is unreachable from a test.

Mini Drill or Application

Pick a function in your codebase that calls an external service (DB, HTTP, filesystem, clock). Without changing its call sites, introduce one seam. Write a characterization test that uses the enabling point to substitute a stub. Confirm the original production call site still works unchanged.

Video and Lecture References

Article References

External Exercises

Depth Path


Source Backbone

Refactoring is the canonical book backbone for this module. Use these sources after attempting the refactor and tests yourself.