← All posts

Rewriting Document Generation Without the Big-Bang Risk

How we replaced a legacy, vendor-backed document engine that touches every product line — turning a high-stakes rewrite into a per-template routing decision that was always one flag flip away from rollback.

By LegalZoom Engineering · · 7 min read

Every LegalZoom order eventually becomes a document. Articles of incorporation, EIN application letters, estate planning bundles, welcome packets. These are the things customers actually pay us for, and many of them are legally meaningful. So when we decided to replace the engine that generates them, we anchored the whole project on the failure we most wanted to avoid. It was never downtime. The worst possible outcome was silently producing a wrong document that a customer files with a government agency.

That single sentence shaped every design choice that followed. The rest of this post walks the major risks of replacing a system this central, and how each piece of the architecture was built to defuse one of them.

For years, generation ran on a commercial template engine from a third-party vendor, fronted by an internal .NET document-delivery service. The flow was a period piece: map answer data into a product-specific request, transform it into vendor-specific XML, submit a render job over OAuth 1.0, poll the vendor every five minutes until the job finished, then pick the finished files up off a Windows network share before storing and uploading them. State was scattered across several SQL databases. It worked, but it was brittle, slow, and most dangerously it hid business logic we didn’t fully control. We set out to move document generation onto a new in-house platform. The question was never whether to do it. It was how to do it without betting the entire product line on a single cutover.

Risk 1: one cutover, everything at once

A document engine sits at the center of the business. It’s wired into every product line and every fulfillment integration. A big-bang replacement means flipping all of that at once and hoping. We weren’t willing to hope, so we stopped thinking about this as a cutover and started thinking about it as routing.

We unbundled the architecture into three layers:

  • An in-house template engine that renders documents (Python tooling, deployed on Kubernetes).
  • A doc-gen API gateway that handles routing, upload, and storage integration, and emits Kafka events after upload.
  • Product and domain services that own payload mapping and document-selection orchestration (Kotlin for most fulfillment orchestration; one domain service is in Go).

The gateway is the migration bridge. It does config-driven, per-template routing: only templates explicitly listed in the routing config go to the new engine. Everything else continues to the legacy vendor, byte-for-byte unchanged.

generate(template_id, payload)
  └─ gateway: is template_id in new-engine routing config?
       ├─ yes → in-house template engine → PDF → storage → Kafka event
       └─ no  → legacy vendor path (unchanged)

A rewrite that touches everything became a per-template decision that touches one thing. The blast radius of any mistake shrinks to a single template.

Risk 2: discovering a problem with no way back

Catching a bad document is only useful if you can undo it instantly. A migration whose rollback is a data-migration unwind is a migration that, under pressure, you can’t actually reverse.

So we made both engines always live and put the new path behind feature flags (LaunchDarkly) in the product and fulfillment layers. The flags are scoped narrowly: per product, per template, sometimes per jurisdiction. We split pre-filing and post-filing phases into separate flags, so turning on one phase never silently changes the other. And we added engine-specific flags so the new-engine rollout could be reverted independently of the broader gateway migration.

That made rollback a non-event. It’s a two-layer traffic reversal: flip the feature flag in the product/fulfillment layer, or remove the template from the gateway’s routing config, and traffic returns to the legacy engine. There’s no state to unwind, because the legacy path always stayed intact and was always one flip away. Decoupling those rollout dimensions mattered as much as the flags themselves. Keeping engine separate from gateway, and pre-filing separate from post-filing, is what stops one change from quietly causing another.

Risk 3: sending a template down the wrong rendering path

Before migrating a template, we classify it by its XML structure into two buckets:

  • Narrative templates (sections, paragraphs, tables, fragments) route to the new template engine.
  • PDF-overlay templates (overlay forms with only text fields) route to a pdf-fill path.

Send a template down the wrong path and the output is wrong. The trap is the hybrid: a template that places narrative fragments onto a PDF background. It looks like an overlay, but pdf-fill can’t reproduce fragment content, so it has to go to the narrative engine. We learned this the honest way. We initially bucketed our Welcome Packets as PDF-overlay, then reclassified them to narrative once we realized they use fragment links inside the overlays. That single misclassification is the whole reason “hybrid → narrative” is now a written rule. The gotchas get written down precisely because we paid for them once.

Risk 4: chasing the wrong definition of correct

The new engine renders differently. The legacy vendor baked PDF design chrome into its output; our in-house engine renders narrative content, using WeasyPrint for the PDF step. For some documents the rendered result legitimately looks different while being completely correct. A validation strategy built on pixel-level comparison would block on cosmetic differences that don’t matter and miss field-level errors that do.

So we validate at migration time, comparison-driven, rather than running always-on production shadow diffing. For each template we render representative payloads and check three things: structure, variable mapping, and key-phrase/key-field accuracy in the output PDF text. For sampled real orders, we do a visual comparison against the legacy production document. The acceptance bar is key-field accuracy, not visual similarity. For one narrative effort we rendered all 35 of 35 templates with every key field verified; a separate set of 44 PDF-overlay templates had their fill coordinates verified before cutover. For Welcome Packets, where the new engine drops the legacy’s baked-in design chrome, we expected only roughly 20–30% visual similarity and said so up front. The criterion that mattered was whether the key fields were correct. They were: 5 of 5 across sampled orders. Choosing key-field correctness as the bar let us migrate documents whose design legitimately changed without getting stuck arguing about cosmetics.

Risk 5: the logic the vendor never told us about

The most dangerous part of this migration wasn’t rendering. It was the orchestration the old vendor did for us without telling us. A single “generate document” request could fan out into a whole set of documents, and the vendor’s template-selection rules decided which ones. Estate planning was the extreme case: one step could mean orchestrating up to roughly fifteen documents whose selection rules had been buried in the vendor for years. Our new doc-gen API is deliberately single-document oriented, which is cleaner, but it means all of that hidden fan-out (selection, retries, rollback, archival, completion tracking) has to be excavated and rebuilt in application code.

There was no shortcut. The multi-document selection had to be reimplemented in domain services, with sequential workflow steps so that no branch silently gets dropped. For estate planning we also replaced a third-party workflow engine with an in-house orchestrator; an early MVP processed its initial batch of orders with zero failures, after the prior engine had given us reliability trouble. If you take one thing into your own migration, budget for this excavation. Legacy engines hide business logic, and the hiding is the hazard.

Risk 6: paying the discovery cost on every product

The first template migration is research. The fiftieth should be muscle memory. The risk in between is that each new product re-learns the whole process from scratch and reintroduces every mistake we already made.

So we built an internal migration accelerator: a knowledge and tracking repo, reusable comparison and test utilities, archived legacy templates, and an AI-assisted migration pipeline that codifies the repeatable steps. Capture the legacy template, classify it into a bucket, stage it, generate the payload mapping (a translator script auto-generates pdf-fill coordinate mappings directly from the legacy overlay definitions), wire the flags, render-test it, and open the cross-repo PRs. The accelerator didn’t make any single migration trivial. What it did was kill the rediscovery cost.

The platform is live in dev and rolling out incrementally across the product line: business formation and post-filing for entities like LLCs and nonprofits, estate planning, and the broader retirement of legacy composition and signing systems. Each new product is a little less scary than the last, because the pattern is the same every time: classify, map, render-test, route, flag, and keep the legacy path one flip away. We’re still rewriting the engine under the moving car. We’ve just made sure that whatever goes wrong, we can always steer back before a wrong document ever reaches a customer.

platform-migration document-generation feature-flags incremental-rollout ai-assisted-tooling architecture

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

More in AI for Platform Migrations

AI for Platform Migrations

How We Used a Grounded Coding Agent to Accelerate a Commerce-Platform Migration

A large commerce migration is a coordination problem before it is a coding problem. Here is how we replaced cross-team handoffs with a spec-driven, pattern-classifying coding agent — and the validation scaffolding that kept it from hallucinating its way into production.

LegalZoom Engineering · · 7 min read

AI for Platform Migrations

Unifying Feature Entitlements Across a Commerce Platform Without Breaking It Mid-Flight

Entitlements are the load-bearing glue between a purchase and what a customer can actually do. Here is how we consolidated two generations of that glue into one authoritative store, migrated hundreds of thousands of historical records idempotently, and never once risked a paid order without its grant.

LegalZoom Engineering · · 8 min read