← Back to home
← Back to the blog
6 min read· DirtFleet team

The batch discipline: how DirtFleet ships a vertical slice per iteration

Schema + lib + REST + UI + tests, one commit, before the next iteration opens. The rule we don't break — and what it costs.

Twenty-plus iterations in, the question we get most from other founders is some variant of how are you shipping this fast. The honest answer is: one rule, applied without exception. Every iteration ships a complete vertical slice before opening the next one. Slice = schema + lib + REST + UI + tests + commit. No exceptions, no “UI later,” no half-built feature flags sitting open for weeks. Here's what that buys you and what it costs.

The rule

An iteration is done when:

  1. The Prisma migration is committed and the schema is the schema we want to live with.
  2. The lib repository functions exist with cross-tenant guards and return discriminated { ok: true, ... } | { ok: false, error: ... } shapes.
  3. A REST endpoint at /api/v1/... calls those lib functions with auth + scope checks.
  4. The in-app UI calls the same lib functions (not the REST endpoint, not a parallel implementation).
  5. Tests cover the lib functions for the happy path + the tenant-isolation path + at least one failure case.
  6. tsc --noEmit passes. vitest run passes. next build passes.
  7. One commit, on main (or fast-forwarded to main within the hour).

If any of those isn't true at the end of the session, the iteration isn't done. Don't open the next one. Don't switch to “just one more feature.” Fix this one first.

Why it works

The temptation, when you're shipping fast, is to keep five threads of work open and merge whichever one finishes first. It feels productive — five PRs in flight, look at all that velocity. In practice it's the opposite. Every open thread is a tax on the next thread: schema conflicts compound, the review burden grows quadratically, and the moment one thread depends on another's migration you're stuck.

One vertical slice at a time forces you to make the architectural calls early — what does the table look like, what does the lib API look like, what does the URL look like — and bake them into one cohesive change. By the time the UI sits on top, every layer below it is settled. There's no going back to add a missing field after the fact, because the field was there from the migration that opened the iteration.

It also makes “done” legible. A feature that's 50% UI, 80% lib, 30% test, 0% docs is impossible to estimate. A feature that's shipped is shipped. The pile of iterations behind you is the same shape as the pile in front — you can actually predict your delivery date.

What it costs

It's slower at the bottom of each iteration. You have to do the schema work, the lib work, the REST work, the UI work, the test work, and the docs work — in series, for every slice. There's no “parallel team” speedup. If your instinct is to specialize people on layers (backend folks on schema, frontend folks on UI), this discipline fights you.

It also doesn't fit refactor-heavy work. Some changes touch every layer of the stack and there isn't a vertical slice to be had — those are their own iterations, and you carve them off as a single thing too. Pretending a refactor is a feature iteration is the failure mode here.

The supporting habits

  • Schema-first batches. Open every iteration with the Prisma migration. If you can't describe the tables, you can't describe the feature.
  • Lib functions are the contract. Both the UI and the public API call lib functions. The REST endpoint is an HTTP wrapper around the lib function, not a parallel implementation. When the lib function changes, both surfaces update at once.
  • Tests live next to libs. If a lib function stabilizes, write the test. If it's still being shaped, don't. TDD ceremony pre-launch creates rigidity without a payoff.
  • Documented decisions. The non-obvious calls (why Capacitor not React Native, why Postgres-as-job-queue, why no GraphQL) live in docs/*. Future-you needs the context to change them; new contributors need the context to not relitigate them.
  • No long-lived branches. Branches in a one-engineer shop are a tax. Push to a worktree branch, fast-forward to main when done, delete the branch. The git history is your project history.

What this is not

This isn't a productivity philosophy or a methodology we want to sell. It's the shape that fell out of trying to ship 25 verticals of software with a tiny team in a single sprint without it falling apart. It works for opinionated B2B SaaS where the data model is the product. It probably doesn't work for a consumer app where design iteration is the product and the schema is incidental.

If you're building something where data integrity matters more than visual polish, where the customers are technical enough to want the public API, and where you'd rather ship eight good things than build twelve half-things — try the rule. One vertical slice at a time. Don't open the next one until this one's done.

→ Integration-first design · → What shipped this sprint · → Try the API