← Case Studies
Case 001saas202612 weeksPersonal

One Engine. Nine Teams. Zero Hardcoded Rules.

A pipeline engine that treats business rules as runtime data instead of hardcoded logic — nine teams, one engine, zero deployments for config changes.

Stack
NestJS
Next.js 15
PostgreSQL
Prisma
Turborepo
Scale
2026
12w
01Context

I designed and built the backend architecture for a lifecycle CRM that manages customer records across nine internal business teams — from first contact through conversion, post-service delivery, and billing. Each team operates in a role-scoped view of the same underlying data, seeing only the stages and fields relevant to their function. The system handles thousands of active records simultaneously, with strict rules governing who can move what, when, and where.

The architecture is a Turborepo monorepo: a NestJS backend, a Next.js frontend, and a shared data layer with enforced import boundaries between what each side of the stack can access.

02The Challenge

Every team owned a different slice of the customer journey. Sales had its pipeline. Customer Success had theirs. Onboarding, Activation, Support, Retention, Billing — each with their own stages, their own required fields, their own rules for when a record could move forward, go backward, or hand off to the next team.

The product evolved constantly. New stages. Changed requirements. Fast-track exceptions. Edge cases that didn't exist when the pipeline was first designed.

The naive approach — service methods, hardcoded stage IDs, conditional field checks — worked for two teams. By the fifth, every product change was a deployment. The gap between "we need this" and "it's live" was measured in days.

The system needed to stop treating its own rules as source code.

Pipeline Flow — cross-team handoffs

The rules weren't complex. They were numerous, and they changed constantly. That's not a programming problem — it's a data modeling problem.

03The Configuration Model

Every pipeline rule moved into the database. A stage is not just a name and an order — it's a complete behavioural specification. Does completing it hand the record to a different team? Can the record be sent backward from here? Are there stages it's allowed to skip ahead to? If it hands off, exactly where in the receiving team's pipeline does the record land?

Fields are equally configurable. Each carries an input type — eleven in total — and a behaviour specification: whether the system populates it automatically rather than waiting for user input, whether that happens on entry or exit, and whether writing to it in one stage should propagate the value across every other stage where that field appears.

Data Model — stage configuration schema
04The Transition Engine

Every stage move — forward, backward, or cross-team handoff — runs through the same engine, on every team, inside a single database transaction.

The engine has no knowledge of which team it's serving. It reads the configuration, executes the sequence, records the history. The team-specific rules are entirely in the data.

Four gates are evaluated in sequence before any write occurs. The handoff gate ensures a handoff-point stage can only go to its configured destination. The cross-team gate ensures records enter another team only through a configured handoff. The sequential gate ensures the target is the next stage by order, same stage, or explicitly whitelisted. A pluggable domain rule layer may impose additional conditions based on record state.

Transition Engine — every move through this sequence

A record was never left in a half-moved state. The transaction either completed fully or it didn't happen.

05The Result

Product changes that used to require a deployment became configuration updates. A new required field on a stage — done without touching code. A fast-track skip path for a specific record type — a single row update. A stage that needed to allow reverting — one flag.

The nine teams stopped being a scaling problem. Each team's pipeline was a different configuration. The engine didn't care.

The integrity decisions had a quieter impact. Support tickets about records stuck in ambiguous states disappeared. Revert operations became deterministic. The history log meant there was always an authoritative answer to "where has this record been, and in what order."

When the domain is complex and evolving, the right investment is an engine that treats business rules as data — not a codebase that encodes them.

06What I'd Do Differently

The field validation layer grew inside the stage management module when it should have been its own service from the start. As the number of input types expanded, the validation logic became harder to test in isolation. That boundary should have been drawn earlier.

I'd also build the configuration admin interface sooner. Removing the need for deployments is half the problem. The other half is giving non-technical users a way to manage the pipeline themselves. That tooling came later than it should have.