DEV Community

Vincent Tran
Vincent Tran

Posted on Originally published at 0xgosu.dev on

Build Wide, Ship Narrow: Decompose Pull Requests After the Code Teaches You

Software teams usually decide how to divide a feature before they build it. An RFC becomes an epic, the epic becomes tickets, and each ticket is expected to become a pull request. This feels orderly because the work arrives at review in small pieces.

It also forces the team to choose its boundaries at the moment it understands the system least.

Before implementation, an engineer can estimate where the API ends, which data model is stable, whether the UI can ship independently, and which migration must come first. But those are still estimates. The first working path often reveals that two planned components are inseparable, a “small” abstraction owns most of the risk, or an allegedly independent frontend slice cannot be evaluated without the backend behavior beside it.

AI coding tools change the cost of responding to that new information. They make exploratory implementation faster, but the important shift is not simply that more code can be produced. The tedious work of reorganizing a completed branch—tracing dependencies, moving changes onto fresh branches, repairing imports, and checking each resulting diff—can now be assisted too.

That enables a different sequence:

  1. Decide what the feature must do and which constraints it must respect.
  2. Build the complete path on an exploratory branch.
  3. Demonstrate the working product and correct the design.
  4. Discover the real dependency boundaries in the finished code.
  5. Recut the work into the smallest safe pull requests.
  6. Review and merge those narrow changes, with removal last.

The idea is not “skip design.” It is to separate product design , which still happens early, from delivery decomposition , which can happen after the code has supplied evidence.

Two Decisions That Should Not Be Confused

A useful plan answers questions that would otherwise turn into expensive rework:

  • What user problem are we solving?
  • What should happen when an external call fails?
  • Which operations require authorization or audit records?
  • What data must remain compatible?
  • Which performance and reliability limits matter?
  • How will we know the result is correct?

Those decisions belong before implementation because they define the problem and its safety boundaries.

A pull-request plan answers a different set of questions:

  • Which changes can merge independently?
  • Which diff gives a reviewer one coherent idea to inspect?
  • Which branch truly depends on another?
  • Where is the safest rollback boundary?
  • Can an intermediate state deploy without breaking users?

These are delivery questions. Some can be predicted, but the implementation often gives better answers. A database endpoint that looked like infrastructure may turn out to contain the central policy decision. Two UI tickets may collapse into one small shared view. A cleanup task may not exist until the replacement path works.

Committing a design document early is still valuable when the feature introduces a new protocol, trust boundary, storage model, or irreversible migration. What should remain provisional is the assumption that the document’s sections must become the final branch graph.

The Wide Build Is a Laboratory, Not a Giant PR

“Build wide” means keeping the end-to-end experiment together long enough to learn from it. It does not mean opening a 5,000-line pull request and asking someone else to untangle it.

The exploratory branch has a private audience: its author and the tools helping them. Commits are save points rather than polished review units. Make one when a concept works, before a risky rewrite, or after a test starts passing. These waypoints preserve recovery and make later archaeology easier, but they are not required to tell the final story.

Working end to end is particularly useful when a feature crosses surfaces. A new user setting might require a schema field, API validation, permissions, a UI control, analytics, and a rollback path. Building only the schema first proves that a column can exist; it does not prove the setting feels right or that the API contract serves the interface cleanly.

The complete path exposes integration mistakes while changing direction is still cheap. It also gives tests something meaningful to exercise. A thin vertical slice can reveal the real invariants faster than several carefully polished horizontal layers.

“A
Design the behavior first, but wait for working evidence before fixing the final review boundaries.

Treat the branch accordingly:

  • Keep it short-lived enough that rebasing does not become its own project.
  • Run tests continuously; “exploratory” does not mean unverified.
  • Record non-obvious decisions in commits or a scratch document.
  • Avoid unrelated cleanup that would obscure the later split.
  • Never treat the wide branch as the only copy of important product reasoning.

The branch is scratch paper, but it should be legible scratch paper.

Demo Before Review

Code review is a costly place to discover that the product is wrong. A reviewer may spend an hour understanding a clean implementation only to learn that the workflow confuses users or that the endpoint shape makes the next screen awkward.

Once the wide build works, show the behavior before asking anyone to read the code. Depending on the change, that can be a preview deployment, a short screen recording, a command transcript, a benchmark, or an API session. Invite the people who understand the user need, not only the people who understand the repository.

This feedback answers a high-value question: should this be the thing the team ships? If the answer is no, rewrite the exploratory branch. Nobody has reviewed a doomed diff, no stack needs rebasing, and no carefully staged migration has to be reversed.

The demo is not a substitute for technical review. It validates the result while review validates the implementation and transfers ownership. The sequence matters: first confirm that the product deserves to exist, then invest human attention in how it works.

Turn the Finished Diff into a Dependency Graph

After product validation, compare the wide branch with main. Do not begin by preserving its commit history. Begin by inventorying the behavior and the dependencies the code revealed.

For each coherent change, ask:

  1. Does it compile and test without the rest of the feature?
  2. Can it deploy without exposing an incomplete or unsafe state?
  3. Does it provide value by itself, or at least enable one clearly named next change?
  4. Can a reviewer understand its purpose without mentally loading the full feature?
  5. Does reverting it have a clear effect?

The answers produce a graph rather than a simple list. Independent backend capabilities can be sibling branches from main. A UI that consumes one endpoint should sit above that endpoint. Instrumentation can often be independent. Removal of the old implementation usually depends on every replacement path and belongs at the end.

“A
Sibling branches reduce rebase coupling; stack only the edges that represent real code dependencies.

This graph is the proposed review plan. A human should approve it before branches are created. An agent can trace changed symbols and move patches, but deciding whether an intermediate state is meaningful, operable, and safe remains an engineering judgment.

Cut Fresh Branches, Not Decorative Diffs

A narrow pull request must be real. Hiding files from a giant diff, asking reviewers to inspect commits in a particular order, or describing “logical sections” in the PR body does not create independent merge and rollback boundaries.

Create each root PR from the latest main. Use a Git worktree when several branches need to be assembled and tested side by side. Move the smallest coherent patch into each branch, then repair whatever the extraction exposes: missing types, implicit setup, tests coupled to later behavior, and imports that belonged to the experiment rather than the product.

Dependent work should branch from the PR it genuinely needs. GitHub’s gh stack models this directly: the bottom branch targets the trunk, and each higher branch targets the layer beneath it so the review shows only that layer’s diff. The tool can maintain and submit the stack, but it cannot decide whether the dependency is legitimate.

Every extracted branch needs its own validation. At minimum:

  • build and type-check it without later branches;
  • run tests for the behavior it owns;
  • verify migrations can coexist with the currently deployed application;
  • confirm hidden or dormant paths remain unreachable until their consumer lands;
  • inspect the actual PR diff, not only the final wide-branch result.

This is where late decomposition pays for itself. If a supposedly independent patch cannot build alone, the failure reveals either a missing prerequisite or a false boundary. Fix the graph instead of disguising the dependency.

Stack Only When the Dependency Is Real

Stacks are useful, but every edge adds coordination cost. Feedback on a lower PR may require rebasing every branch above it, rerunning CI, and asking reviewers to distinguish old changes from new ones.

The default should therefore be siblings from main. Use a stack when one change cannot be expressed, built, tested, or understood without another. A frontend view that imports a new API type is a real dependency. Two endpoints created during the same experiment are not automatically dependent. Neither are two cleanup patches that merely touch neighboring files.

Small-batch guidance from DORA emphasizes independent, valuable, testable units and notes that small batches are especially important when AI increases delivery speed. Late decomposition should preserve that goal. It changes when the batch boundaries are discovered; it does not excuse shipping a coupled batch under several PR numbers.

A good stack is short and obvious. If the graph contains long chains, multiple diamonds, or branches that repeatedly exchange changes, the system may need a stabilizing interface, feature flag, or branch-by-abstraction step before review can be clean.

Put Deletion Last

Replacing an old path combines two different risks: whether the new path works and whether the old path is truly unused. Mixing construction and deletion makes both harder to review and rollback.

Ship removal in a final PR after the replacements have landed and, when possible, after production evidence shows they are carrying traffic correctly. A deletion-only diff is unusually honest. Reviewers can focus on references, compatibility, fallback behavior, documentation, and operational scripts without also proving a new architecture.

This ordering enables a safer rollout:

  1. Add foundations that do not change behavior.
  2. Add the new path behind a flag or dormant entry point.
  3. Add consumers and enable the path gradually.
  4. Observe errors, latency, and adoption.
  5. Remove the old path and its compatibility code.

The final PR often deletes more than the feature added. That is a useful signal: the team has finished the replacement rather than leaving two systems to coexist forever.

Review Is for Understanding, Not Ceremonial Approval

AI can catch formatting errors, suspicious patterns, missing tests, and obvious inconsistencies. It can summarize a diff and answer questions about call sites. These capabilities reduce mechanical work, but they do not eliminate the human purposes of review.

Review transfers ownership. Someone must understand why the change belongs in this layer, which assumptions make it safe, how it will fail, and what the team will do six months later. Microsoft Research has argued that code review is a costly social and knowledge process, not simply a dependable bug-finding gate. That framing matters more when the author did not type every line.

The author should read each extracted diff as if it came from another engineer. Explain the intent without leaning on hidden context from the wide branch. Re-run the important path. Challenge surprising abstractions. Remove generated complexity that no longer serves the validated result.

Small PRs do not guarantee good review, but they make deep review possible. A narrow diff gives both author and reviewer a bounded object they can hold in working memory and a focused conversation they can complete.

Where the Workflow Breaks Down

Late decomposition is not a universal default.

Irreversible migrations need early sequencing. If a schema or data rewrite must roll through production in compatibility phases, the order is part of the design. Build and test the full migration, but preserve expand-migrate-contract boundaries from the start.

Long-running branches accumulate integration risk. A wide experiment that lasts weeks becomes expensive to rebase and may hide conflicts until the split. Set a time box. If exploration grows beyond it, extract stable foundations early.

Some features have one atomic release boundary. A cryptographic protocol change, tightly coupled compiler pass, or externally versioned API may not deliver incremental user value. Narrow PRs can still improve review, but deployment may remain all at once.

A weak test suite makes recutting dangerous. The final branch may work because of accidental ordering or unrecorded local state. Without reliable build, integration, and migration checks, moving patches between branches can silently change behavior.

Generated volume can outrun review capacity. Faster decomposition does not mean reviewers can absorb unlimited change. If the resulting PRs sit open, the process produced tidy inventory rather than delivery.

Exploration can become an excuse for architectural drift. The wide branch still needs constraints, observability, security review, and stop conditions. “We will clean it up later” is not a plan unless cleanup is actually extracted, reviewed, and merged.

A Practical Team Policy

Teams adopting this workflow can keep it disciplined with a short policy:

  • Use it for cross-surface features and uncertain refactors, not every one-file change.
  • Require a written design first for new trust boundaries, protocols, or migrations.
  • Time-box the wide branch and keep it continuously testable.
  • Validate the working behavior before code review begins.
  • Require a dependency graph and human approval before branch extraction.
  • Branch independent PRs from main; stack only real dependencies.
  • Give every PR its own build, tests, deployment safety, and rollback story.
  • Keep removal in a final, focused PR.
  • Track time from first PR opened to last PR merged, not only time to split.

Measure outcomes rather than admiring the method. Useful signals include review latency, number of rebase rounds, escaped defects, revert scope, age of the final cleanup PR, and how often extracted branches failed independent validation. Compare those with conventionally planned work of similar size.

The goal is not to maximize the number of pull requests. It is to put uncertainty in the cheapest part of the process. Explore while the code is private, validate while changes are easy, and spend reviewer attention only after the product and the dependency boundaries have earned it.

The original Build Wide, Ship Narrow article describes the workflow that prompted this model. The Hacker News discussion captures the community’s debate about branch history, stacked reviews, and whether late slicing truly improves delivery. The durable lesson is simpler than any particular tool: decide the problem early, but let evidence decide the final shape of the review.

Top comments (0)