Skip to main content
Sean Pollock
← Writing

From Lambda Sprawl to Monorepo: How We Went from Less Than 1 Deploy a Week to 11

June 1, 2026

When I joined Fig in 2023, the codebase had been built by a dev shop. It worked. It processed loan applications, pulled credit bureau reports, returned offers. But shipping anything new was an ordeal.

The team was deploying less than once a week. Features that should have taken days were taking months. The developers weren't slow. The process was.

Fixing it was the first thing I put on the roadmap.


The situation we inherited

Four separate AWS Lambda microservices, each in its own repository, one being a TypeScript/React frontend repo. The frontend was bundled and served via S3 and CloudFront, with the infrastructure managed in Terraform.

Each repo had five long-lived branches, mirroring Salesforce's environment structure: develop, integ, uat, preprod, main.

To ship a feature, the process was:

  1. Build it on a feature branch, merge to develop
  2. Open a PR to integ, merge
  3. Open a PR to uat, merge
  4. Open a PR to preprod, merge
  5. Open a PR to main, merge

Five PRs. Per repo. Per feature. And if the feature touched multiple services (which most did), you multiplied that by however many repos were involved.

The failure mode was predictable. Bugs discovered late in the chain meant going back to develop and re-promoting upward. Branches drifted. A change merged to staging that never made it back to the feature branch. Merge conflicts to resolve three, four, five times. A 2-line hotfix blocked for two weeks because a half-finished epic was sitting in uat not ready for production.

The branching model wasn't some idiosyncratic mistake by the dev shop. It made sense given the constraints they were working under. Salesforce environments (the real backend for this app) drove the structure, and they built around it. But it was fundamentally the wrong model for shipping software quickly, and by the time I joined it was the primary constraint on velocity.


What we changed

Three things, applied together. Each one necessary. None sufficient on its own.

1. Monorepo

We consolidated all four Lambda microservices including the frontend into a single repository.

The Python Lambda functions became a Django application. This sounds more dramatic than it was. A lot of it was copy-paste, moving handler functions into Django views and wiring up routing. The underlying business logic didn't change. We weren't rewriting; we were reorganizing.

The frontend moved too. Instead of a separate repo deploying to S3/CloudFront via Terraform, it became a static site built and deployed to Render's CDN, defined in the same render.yaml as the backend services. One config file for the whole system's infrastructure.

The monorepo used a standard structure with apps/ for each deployable unit and packages/ for shared code.

The immediate benefit: one PR, one review, one place to look when something breaks. The coordination overhead of multi-repo releases disappeared.

2. Trunk-based development

One branch: main. All PRs target it. No long-lived environment branches.

The rule is simple: if your feature isn't ready for production, it goes behind a feature flag. You still merge continuously. You still deploy continuously. The flag controls what users see.

This forces you to keep branches short-lived and changes small. It's a discipline as much as a technical decision. The payoff is that the codebase always represents a deployable state and "the release" stops being a scheduled event that requires coordination.

For a small team, the feature flag overhead is low. We used PostHog flags, which were lightweight, easy to instrument, and doubled as experiment infrastructure when we wanted to measure the impact of a change before fully rolling it out.

3. Ephemeral preview environments on Render

This is the change most people underestimate, and it's the one I'd implement first if I were doing it again.

The old model had shared environment branches. Multiple developers, multiple features, one environment. You'd finish a feature, try to test it, and find that staging was occupied with someone else's half-finished work. Or the configuration had drifted from production in some subtle way that only showed up after you shipped.

With Render's preview environments, every pull request automatically spins up a complete, isolated clone of the production environment with that branch's changes applied. It goes away when the PR closes.

The configuration required to enable this:

# render.yaml
previews:
  generation: automatic
  expireAfterDays: 3

Three lines. Render handles provisioning, networking, environment variables (inherited from production with overrides for the preview context), and teardown.

A few things worth knowing if you're evaluating this:

Stateful services need thought. Preview environments work cleanly for stateless services. If your app has a database, Render will spin up a preview database and run the migrations. But you need to decide what data it gets and design a seeding process, otherwise you will be doing a lot of manual data creation in your test envs.

Webhooks require additional routing. Third-party services send webhooks to specific URLs. A preview environment at https://pr-123.render.com won't receive webhooks targeted at your production domain. We solved this with Hookdeck (a topic for a separate post), but it's worth planning for upfront.

Cost is real but manageable. Preview environments consume cloud resources. Render's per-service pricing meant active previews weren't free. The expireAfterDays setting handles cleanup for abandoned branches, and we kept an eye on active previews during busy sprints. At our scale it was well worth it, but worth modelling for your own usage patterns.


The result

Under 1 deploy a week → 11 a week, within a few months of completing the migration.

The less obvious result: the nature of conversations changed. We stopped talking about "when can we release this" and started talking about "what should we build next." Deployment became operational background noise rather than a coordinated team event.

There's a version of this transformation that takes six months and involves a careful migration plan, feature flag audits, and extensive testing of the new environment setup. Ours took closer to one sprint for the monorepo consolidation and another sprint to get Render fully configured. The reason it was fast: we weren't changing what the software did, just where and how it lived.


The developers weren't slow. Yours probably aren't either. Monorepo, trunk-based development, and ephemeral previews have been standard practice for years. The gap between knowing about them and actually doing them is where most teams are stuck. Treating the migration as a focused project with a real deadline was the main thing that made ours happen quickly.