Skip to main content

Type-First Development

What if the compiler could tell you what to build next?

DOMAIN (contracts) → INFRASTRUCTURE (repos) → APPLICATION (logic) → PRESENTATION (UI)
│ │ │ │
▼ ▼ ▼ ▼
typecheck typecheck typecheck typecheck
green? ──→ green? ──→ green? ──→ green? = DONE

Flow engineering tells you WHAT to build. Type-first development tells you HOW to build it — start at the domain, let TypeScript errors pull you outward layer by layer. The compiler becomes the methodology.

The Principle

Three ideas that compound:

IdeaWhat It Means
Domain-firstStart at the center. Contracts define what exists. Every other layer adapts.
Type-drivenChange a type, run typecheck. Red squiggles ARE your todo list.
Constraint satisfactionThis isn't creative design — it's satisfying constraints. Agents excel at this.

Why this matters for AI products: agents hallucinate architectures. Give them a concrete verification loop and they can't go wrong. Infinite search space becomes a deterministic algorithm.

The Algorithm

The Boris Rule (paraphrasing Boris Cherny): Give Claude a concrete verification loop and tell it to iterate until checks pass.

1. Make domain change (ports, entities, DTOs)
2. Run typecheck
3. Red? FIX IMMEDIATELY — never proceed with errors
4. Green? Move outward to next layer
5. Repeat until all layers green
6. All green = done

Never batch fixes across layers. Never proceed with red. This is constraint satisfaction, not design.

Pre-Flight

Before any change, answer four questions:

QuestionWhat It Reveals
What outcome? (1-3 sentences)Maps to Outcome Map
What binary measure makes it "done"?Test, metric, or demo path
Which layer? (domain / infra / app / UI)Where to start
Does a generator cover this?Use it before hand-coding

The flow engineering maps answer the strategic questions. Pre-flight answers the tactical ones.

Layer Model

Dependencies point inward. Updates propagate outward.

┌─────────────────────────────────────────────────┐
│ PRESENTATION │
│ ┌─────────────────────────────────────────┐ │
│ │ APPLICATION │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ INFRASTRUCTURE │ │ │
│ │ │ ┌─────────────────────────┐ │ │ │
│ │ │ │ DOMAIN │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ Ports, Entities │ │ │ │
│ │ │ │ DTOs, Events │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ └─────────────────────────┘ │ │ │
│ │ │ Repos, Adapters │ │ │
│ │ └─────────────────────────────────┘ │ │
│ │ Use Cases, Orchestrators │ │
│ └─────────────────────────────────────────┘ │
│ Components, Actions, Routes │
└─────────────────────────────────────────────────┘
LayerContainsRule
DomainPorts, entities, DTOs, eventsSource of truth. Never imports outward.
InfrastructureRepositories, adaptersImplements domain ports. Only layer touching the database.
ApplicationUse cases, orchestratorsComposes infrastructure through ports. Business logic lives here.
PresentationComponents, actions, routesConsumes application layer. Transforms contracts into views.

When a domain contract changes, the compiler lights up every file that needs updating — outward through infrastructure, application, presentation. Red squiggles are breadcrumbs.

Maps to Code

Each flow engineering map produces specific code artifacts:

MapCode ArtifactsLayer
Outcome MapPorts, DTOs, domain eventsDomain
Value Stream MapUse cases, repositories, adaptersInfrastructure + Application
Dependency MapComposition roots, task orderingApplication + Presentation
Capability MapGenerators, skills, work chartsPlatform
A&IDAgent configs, instrument schemasAll layers

This is the bridge between pictures and products. The maps aren't documentation ABOUT the code — they produce the code.

Type Boundaries

Data transforms explicitly at each layer boundary:

┌──────────────┐   serialize()   ┌──────────────┐   map()   ┌──────────────┐
│ DOMAIN │ ──────────────→ │ CONTRACT │ ────────→ │ VIEW │
│ │ │ │ │ │
│ Rich types │ │ Wire-safe │ │ UI-ready │
│ Date objects │ │ ISO strings │ │ Formatted │
│ Numbers │ │ Strings │ │ Numbers │
└──────────────┘ └──────────────┘ └──────────────┘
BoundaryTransformationWhy
Domain to ContractDate becomes string, number may become stringJSON safety, precision
Contract to Viewstring becomes number, dates formatted for displayUI consumption

Each transformation is an explicit function. No implicit coercion. The compiler catches every mismatch.

The Trap

// Domain: amount is a number
interface Deal { amount?: number }

// Contract: amount becomes a string (precision)
interface SerializedDeal { amount: string | undefined }

// View: amount is a number again (for calculations)
interface DealView { amount?: number }

// The mapper that makes it safe
const toDealView = (s: SerializedDeal): DealView => ({
amount: s.amount ? Number(s.amount) : undefined
})

Without the mapper, you assign a string to a number. TypeScript catches it. Without TypeScript, your UI silently displays "150000" where it should calculate 150000. The type boundary IS the safety net.

Diagnosis

When type errors surface, trace the boundary:

StepQuestionAction
1Where is the boundary?Domain to Contract? Contract to View? Action to Hook?
2Is a transformation missing?Date to string? number to string?
3Is the contract type wrong?Does SerializedX match what the serialize function returns?
4Is the consumer wrong?Is the component expecting domain types instead of contracts?
5Is there a missing mapper?Does a transformation exist between contract and view?

80% of type errors at layer boundaries are serialization mismatches. Check the boundaries first.

Generator-First

Never hand-code what a generator can scaffold. Generators enforce correct layer order automatically — domain first, then infrastructure, then application, then presentation. The generator is a capability map turned into a tool.

When a pattern occurs more than twice, it becomes a generator. When a generator exists, using it is mandatory. This is how capabilities compound — codified knowledge replacing manual effort.

Checklists

Before Work

  • Outcome and binary success measure defined
  • Relevant flow map identified
  • Primary layer identified (domain / infra / app / presentation)
  • Existing generators and patterns checked

During Work

  • Domain changes first — no domain file imports outward
  • TypeScript errors used as breadcrumbs, layer by layer
  • Public contracts in domain, internal validation colocated with consumers

Before Commit

  • All touched layers pass typecheck
  • No any or @ts-ignore added to silence errors
  • If a pattern repeated, generator considered
  • Relevant flow maps updated with new knowledge

Context

References