← Back to list

Narrative-Driven Development

Write the Story Before the Chapters

Carlos Mimoso · 2026-03-05 00:09 · 2 claps · 3.1 min read
#ddns #narrative-driven #typescript #workflow #programming
Open on Medium ↗
Wiki topics: LIT · Literature & Writing 💻 · Programming 🌐 · Web Development ✍️ · Writing & Creative

Narrative-Driven Development

Write the Story Before the Chapters

Architects don’t start with bricks. They start with blueprints — documents that describe the shape of the thing before any material is cut, readable by people who will never hold a trowel.

Most code works the other way around. You write the validation function, then the database call, then the email sender, then wire them together in a function that grows like an attic nobody planned.

What if you wrote the blueprint first?

TDD’s Older, More Literate Cousin

TDD gave us a beautiful inversion: define what should happen before writing the code that makes it happen. The test is a contract. The implementation fulfils it.

Narrative-driven development takes this one level up. Instead of starting with the unit, you start with the whole: what is this workflow? What are its chapters? In what order do they unfold?

You write the table of contents before the chapters. And here’s the trick — it compiles.

The Blank Page

You’re building a content publishing pipeline. Editors draft articles, and hitting “publish” kicks off a sequence: validate, moderate, generate SEO metadata, schedule, notify subscribers.

Most developers would start at the bottom — write the moderation service, the SEO generator, the notification dispatcher — then stitch them together in a function nobody will enjoy reading six months from now.

Instead, start with the story:

import { createFlow } from '@celom/prose';

export const publishArticle = createFlow<PublishInput, PublishDeps>('publish-article')
  .validate('validate draft', validateDraft)
  .step('moderate content', moderateContent)
    .withRetry({ maxAttempts: 2, delayMs: 500 })
  .step('generate seo metadata', generateSeoMetadata)
  .transaction('schedule publication', schedulePublication)
  .step('notify subscribers', notifySubscribers)
    .withRetry({ maxAttempts: 3, delayMs: 1_000, backoffMultiplier: 2 })
  .event('content', publishedEvent)
  .build();

That’s not code pretending to be documentation. That is the documentation. It also happens to execute.

The Unwritten Chapters

Here’s where NDD diverges from just “having a nice API.” The step functions don’t need to exist yet:

// steps/moderate-content.ts
export async function moderateContent(
  ctx: FlowContext<PublishInput, PublishDeps, {}>
) {
  throw new Error('Not implemented');
  return { moderationResult: {} as ModerationResult };
}

// steps/generate-seo-metadata.ts
export async function generateSeoMetadata(
  ctx: FlowContext<PublishInput, PublishDeps, { moderationResult: ModerationResult }>
) {
  throw new Error('Not implemented');
  return { seo: {} as SeoMetadata };
}

The flow compiles. TypeScript already knows generateSeoMetadata has access to ctx.state.moderationResult because moderateContent returns it. You've defined the data contract between chapters before writing a single line of business logic.

In TDD, you write a test that fails, then make it pass. In NDD, you write a flow that compiles but throws, then fill in each step until it runs.

The flow is your spec. The steps are your implementation. The gap between them is your backlog.

Legibility as a Feature

There’s a second audience for this blueprint, and they don’t have an IDE.

Product managers, tech leads, new hires — they can all read the flow definition and understand what the system does without tracing error handling or decoding which catch belongs to which try.

When someone proposes adding a plagiarism check between moderation and SEO generation, the conversation becomes spatial:

export const publishArticle = createFlow<PublishInput, PublishDeps>('publish-article')
  .validate('validate draft', validateDraft)
  .step('moderate content', moderateContent)
    .withRetry({ maxAttempts: 2, delayMs: 500 })
  .step('check plagiarism', checkPlagiarism)        // <- new chapter
  .step('generate seo metadata', generateSeoMetadata)
  .transaction('schedule publication', schedulePublication)
  .step('notify subscribers', notifySubscribers)
    .withRetry({ maxAttempts: 3, delayMs: 1_000, backoffMultiplier: 2 })
  .event('content', publishedEvent)
  .build();

One line. The narrative gains a chapter. The story still reads front to back.

The Whole Before the Parts

Traditional development is bottom-up: build components, compose them, hope the composition makes sense. TDD is unit-first: prove each piece works, then integrate. Neither prioritises the overview.

NDD is top-down. You define the workflow’s shape — steps, order, data dependencies — before any implementation exists. The type system enforces it. Change the order and the compiler tells you if the data contract breaks. Remove a step that produced state consumed downstream and you’ll know before you run a single test.

The spec is the code. They cannot disagree because they are the same artifact.

When the Implementation Arrives

Eventually you fill in the chapters. Each step does one thing, returns state:

export async function moderateContent(
  ctx: FlowContext<PublishInput, PublishDeps, {}>
) {
  const result = await ctx.deps.moderationService.check(ctx.input.body, {
    signal: ctx.signal,
  });

  if (result.flagged) {
    throw ValidationError.single('body', `Content flagged: ${result.reason}`);
  }

  return { moderationResult: result };
}

export async function generateSeoMetadata(
  ctx: FlowContext<PublishInput, PublishDeps, { moderationResult: ModerationResult }>
) {
  const seo = await ctx.deps.seoEngine.generate({
    title: ctx.input.title,
    body: ctx.input.body,
    contentRating: ctx.state.moderationResult.rating,
  });

  return { seo };
}

Notice generateSeoMetadata uses ctx.state.moderationResult.rating — produced by the previous step. Data flows through the narrative. Each chapter picks up where the last left off, and TypeScript guarantees the handoff.

By the time you’re implementing steps, the architecture is settled. You’re filling in a shape that already exists.

Write the story first. The chapters will follow.

This is the second in a series of four articles about [@celom/prose](https://medium.com/@celom/replacing-200-lines-of-nested-try-catch-with-a-declarative-workflow-7553cf810818). Next: Testability — how the architecture that makes flows readable also makes them trivially testable.


메타데이터
post_id
02442f539ec8
slug
narrative-driven-development-02442f539ec8
url
https://medium.com/@celom/narrative-driven-development-02442f539ec8
canonical_url
https://medium.com/@celom/narrative-driven-development-02442f539ec8
author_url
https://medium.com/@celom
status
ok
fetched_at
2026-06-20 20:29:01