Unifying a Business Filings Platform Around Core APIs and Events
How we replaced a tangle of overlapping post-purchase systems with one filing domain — built on core APIs, BPMN workflows, and events, anchored by a single canonical business record — without halting the business.
By LegalZoom Engineering · · 7 min read
When a small business owner files a document with a state agency, forming an LLC, filing an annual report, registering in a new state, a lot has to be true at once. The filing has to be valid for that state and that entity type. The system has to know what the business is allowed to file, what it should file next, and where its last filing stands. And it has to do all of that for thousands of permutations of state, entity type, and product.
For years at LegalZoom, the systems that answered those questions had grown apart. Formations, fulfillment, compliance, and a layer of legacy intermediaries had each evolved to own a slice of the truth. That worked when each product was built on its own. It stopped working when we wanted them to behave like one platform. This is the story of the architecture we landed on: a single filing domain built on core APIs, BPMN workflows, and events, anchored by one canonical business record, and the path we took to get there without stopping the business.
The target: one domain that owns the filing question
Picture the end state first, because it organizes everything else. Both business formations and ongoing compliance filings enter through one set of core APIs: shared capabilities like start a filing and see my filings, replacing the patchwork of product-specific info layers. Behind that surface, four parts carry the weight, and each maps to a question the old stack could not answer cleanly.
A single filing domain has to give one reliable answer to the four things that matter: what can this customer file, what should they file, what’s their status, and what happens next. The fragmented stack gave four answers. Filing logic lived in formations delivery, lifecycle state lived in a set of state machines, compliance rules lived somewhere else again, and a collection of legacy intermediaries, answer stores, entity-overview systems, order-injection workers, sat in between, each reconciling a slightly different shape of the same data. The same rules were duplicated across systems and drifted out of sync, so the same question got different answers depending on who you asked. Every new state or product meant re-implementing logic that already existed somewhere. The runtime path made it worse: starting a single filing meant hopping through a questionnaire, into a checkout layer, through a state machine, into an order-staging step, through an order-injection worker, and finally out as an event. Every hop lost observability, and end-to-end injection latency was measured in minutes.
Per-product workflows over a generalized engine
Fulfillment orchestration runs as BPMN processes on Camunda, modeled per filing product, each with its own launcher and process definition. We deliberately resisted building one monolithic, infinitely configurable engine. A generalized engine sounds elegant until it becomes a second programming language that only a few people understand. Per-product workflows keep each filing’s logic legible to the team that owns it, at the cost of some repetition we can refactor into shared delegates.
The same instinct shaped the runtime stack underneath. Orchestration is Spring Boot, Kotlin, and Camunda. There was a credible path to a parallel TypeScript/state-machine approach for some flows, and in isolation it would have been fine. But a divergent runtime stack means duplicated logic and two sets of operational muscle memory. We standardized and absorbed the constraint rather than let a second platform quietly grow.
Events as the connective tissue
Kafka connects commerce, filing, and fulfillment in real time. A checkout or order-placed event and fulfillment-order-update events flow onto the bus; a workflow launcher consumes them and dispatches the correct BPMN process. The simplified end-to-end path now looks like this:
schema/validation → core filing API → Kafka order/fulfillment event
→ workflow launcher selects product launcher
→ Camunda BPMN process executes
↳ delegates: doc-generation, RPA bots, print & ship, notifications
→ finalized facts projected into the canonical business record
Collapsing the old multi-hop route into a single call into the service that owns the filing interface took workflow injection from minutes down to a couple of seconds.
Schema as a hard contract, owned at the edge
A versioned rules-and-schema layer owns the canonical filing DTOs and runs questionnaire validation at the edge. Crucially, fulfillment imports that canonical contract. That makes a schema change a hard prerequisite: it lands before any fulfillment or API change can build against it. We define the rules in TypeScript with a validation library (we use Valibot) and treat them as published artifacts, so the questionnaire, the API, and the workflow all validate against the same source of truth rather than three hand-maintained copies.
This is the part of the architecture that did the most quiet work, because it replaced coordination with a contract. The old way was a slow, error-prone dance of keeping the questionnaire, the API, and the workflow manually in agreement. Making the schema layer a hard, versioned prerequisite that everything else imports eliminated that dance.
The line between in-flight and committed
This is the subtlest decision and the one we’d defend hardest. In-progress filing answers are stored immutably in Postgres inside the filing workflow, because they are transient orchestration state: they belong to one filing in flight. Only after a filing succeeds do we project the finalized, canonical subset of facts into the canonical business record, the durable system of record for the entity, its lifecycle, and its compliance state.
The trap we were avoiding is split-brain. If both the workflow and the entity record can claim to hold the truth about a business while a filing is mid-flight, they will eventually disagree, and you will spend years reconciling them. By making projection a one-way, post-success step, the business record only ever reflects committed reality. Most of our historical pain came from multiple systems each believing they owned the same fact. Naming a single canonical record and being disciplined about when facts enter it removed a whole class of bugs before they could exist.
That canonical record is itself built by consolidating internal entity data, Secretary of State filings, and other public-record and third-party data sources into one unified company model through ETL pipelines, so downstream systems stop reconciling multiple data shapes. It then becomes the foundation for a compliance intelligence layer that combines live data with business-record context to determine filing eligibility, track history, and power next-best-action recommendations and auto-filing.
Getting there while the business kept filing
A target architecture is only as good as the path to it, and ours had to keep filing live documents with state agencies the entire time. We migrated with a strangler pattern: legacy paths stay alive product by product while new ones come up beside them. During transition we maintain dual writes to legacy systems, the legacy answer stores and the old formations codebase, until per-product decommission switches ship. Dual writes are genuinely unpleasant; they are extra failure modes and extra reconciliation. But they are what let us rebuild the machine underneath without halting the business, and we treated them as scaffolding to be torn down, not a permanent bridge.
Not every product needed the same treatment. We tiered the migration to product maturity. Some products moved behind a new API surface with no fulfillment change at all; some got a new API over an existing launcher; some needed a full new workflow module and API surface. Choosing the lightest pattern that worked per product kept the migration moving instead of forcing every product through the heaviest path.
One thing we were careful about: AI lives in our delivery loop, not the filing runtime. Filings are regulated and deterministic, and that’s where we want them. But the migration itself is enormous cross-repo work, and that is exactly where LLM-assisted tooling earns its place. We use it to auto-PR proposed schema updates to the rules layer, to explore codebases and draft cross-repo changes during migration, and to run an autonomous RCA agent that triages incidents and posts root-cause summaries, and can open fix PRs, while a human still owns the official RCA.
We hold the architecture to outcomes, not aesthetics. We track it against the things a filing platform actually has to get right: how much of the work runs straight through without human touch, how quickly a filing moves end to end, how accurately we judge what a business is eligible to file, and how completely the canonical record covers each entity’s data and compliance state. Those measures keep “unify the platform” from becoming an architectural vanity project.
What the foundation makes possible
With one filing domain in place, the work shifts from untangling to extending. A new state or product is increasingly a matter of defining its canonical schema and modeling its workflow, rather than re-deriving rules across four systems. The compliance intelligence layer gets sharper as the canonical record gets richer, which moves more filings from manual to straight-through and, eventually, toward genuinely proactive: telling a business what’s due before they have to ask.
Regulated workflows reward exactly the discipline this architecture is built on. A single source of truth, hard contracts, and a clean line between what’s in flight and what’s committed. That’s a foundation we’re glad to be building on.
We're building this — want in?
If shipping pragmatic, AI-native systems at the scale of millions of small businesses sounds like your kind of problem, we'd love to talk.
See open roles