ThisCalculator

Everyday

Splitting a Funnel Builder into 16 Bounded Contexts

Updated · Standard formula, shown below

Splitting a Funnel Builder into 16 Bounded Contexts

This article explains why a funnel builder—a tool for creating marketing landing pages and checkout flows—was broken into 16 bounded contexts, and what that architectural decision cost in terms of complexity, coordination, and delivery speed. The short answer: the split was done to isolate frequent changes, enable independent scaling, and reduce merge conflicts, but it introduced significant overhead in distributed transactions, cross-team communication, and operational tooling.

Background: What a Funnel Builder Does

A funnel builder is a visual drag-and-drop application that lets marketers assemble a sequence of pages—landing pages, sales pages, order forms, thank-you pages—without writing code. Users place elements like text blocks, images, videos, countdown timers, and payment buttons onto a canvas, then publish the funnel to a custom domain. Behind the scenes, the system must handle page rendering, asset storage, form submissions, payment processing, email automation, A/B testing, analytics, and user permissions.

Monolithically, all these features live in one codebase. That works well for a small team and a simple product. But as the feature set grows, the codebase becomes large, and every change risks breaking an unrelated part. Teams also step on each other’s toes: a change to the page editor might conflict with a payment integration branch. The typical response is to modularize—split the code into separate services, each responsible for a slice of the domain.

In this case, the team identified 16 distinct business capabilities, each of which could be owned by a separate team and deployed independently. These included page templates, element rendering, asset storage, form submissions, payment processing, subscription management, email notifications, user accounts, team permissions, analytics events, A/B test configuration, funnel publishing, domain mapping, caching, audit logging, and webhook delivery.

Why Split into 16 Bounded Contexts?

The primary motivation was to reduce coupling between frequently changing parts of the system. For example, the page editor changes several times a week, while payment processing changes only when a new gateway is added or a compliance rule changes. If both live in the same deployable unit, every editor change requires a full regression test of the payment flow, slowing delivery.

Another driver was team autonomy. With 16 contexts, each team can own its service end-to-end: database schema, API, and deployment pipeline. They can choose the technology that fits the problem—a relational database for payments, a document store for templates, a key-value store for caching. They can also scale independently: the asset service might need more instances during a product launch, while the payment service stays stable.

Finally, the split was intended to improve fault isolation. If the email service crashes, the rest of the funnel builder should keep working. In a monolith, a memory leak in one module can bring down the entire application. With separate services, a failure is contained.

How the 16 Contexts Were Identified

The team used event storming workshops to map the business processes. They gathered product managers, developers, and designers and walked through the lifecycle of a funnel: create, edit, publish, collect leads, process payments, send emails, and analyze results. For each step, they identified domain events (e.g., “FunnelPublished�, “PaymentSucceeded�) and the commands that trigger them (e.g., “PublishFunnel�, “ChargeCustomer�).

From these events, they grouped related commands and queries into aggregates. For instance, all actions around a user’s account—login, password reset, profile update—formed one context. All actions around a funnel’s structure—adding a page, moving an element—formed another. The grouping was guided by the principle that a context should contain only the data and logic that changes together.

They also considered the “ubiquitous language� of the domain. Marketing terms like “lead�, “conversion�, and “campaign� appear in multiple contexts, but they mean slightly different things. The team defined each term within its own context to avoid ambiguity. For example, a “lead� in the form-submission context is a raw record, while a “lead� in the analytics context is an aggregated count.

What It Cost: Coordination and Complexity

The most immediate cost was coordination overhead. Instead of one codebase, there are 16 repositories, each with its own CI/CD pipeline, versioning, and monitoring. A simple feature that spans two contexts—say, adding a new field to a form and storing it in the CRM—requires changes in two services, two pull requests, and a coordinated deployment. The team had to introduce a shared API contract and versioning strategy to avoid breaking changes.

Distributed transactions became a major pain point. For example, when a user submits a form, the system must save the submission, send an email, and update analytics. If these are in separate services, a network failure after saving but before sending the email leaves the system in an inconsistent state. The team had to implement an outbox pattern: the form service writes an event to its own database, and a separate worker reads that event and calls the email service. This added latency and required careful monitoring.

Operational tooling also grew. Each service needs its own logging, metrics, and alerting. The team had to build a shared observability stack that aggregates logs from all services and correlates them with a trace ID. Without that, debugging a failed funnel publish across three services would take hours.

Common Problems and How to Avoid Them

One common mistake is splitting too early. If the team is small and the product is not yet stable, the overhead of 16 services will slow down iteration. The team in this case had already reached a certain scale—around 20 developers and a mature feature set—before attempting the split. For a smaller team, a modular monolith might be a better intermediate step.

Another problem is data duplication. Each context has its own database, and the same data—like a customer’s email—may exist in several places. The team had to decide which context is the source of truth for each piece of data and implement synchronization via events. This is not just a technical issue; it requires product decisions about data ownership.

Finally, testing becomes harder. End-to-end tests that span multiple services are slow and flaky. The team adopted contract testing: each service tests that it can consume the API of its dependencies, using mocked responses. This catches breaking changes early without requiring a full integration environment.

Comparing with Alternatives

The 16-context split is not the only option. A modular monolith keeps all code in one deployable unit but organizes it into internal modules with strict boundaries. This gives you the code-level separation without the network overhead. It is easier to test and deploy, but you cannot scale modules independently, and a bug in one module can still bring down the whole process.

Another alternative is a smaller number of larger services—say, 4 or 5. That reduces coordination overhead but increases the blast radius of a change. The team chose 16 because each context was small enough to be understood by a single team, but that granularity came at the cost of more moving parts.

Cost-Benefit Summary

In the end, the split achieved its main goals: deployment frequency increased, merge conflicts disappeared, and each team could work autonomously. But the cost was real: roughly 20% of developer time went to infrastructure and coordination rather than feature work. The team also had to hire or train engineers with experience in distributed systems—event-driven architecture, message brokers, and observability—which is a scarce skill set.

If you are considering a similar split, weigh the benefits against the operational burden. Start with a monolith, add modules as the codebase grows, and only split into services when the pain of the monolith—deployment conflicts, scaling bottlenecks, team bottlenecks—outweighs the pain of distribution.

Frequently Asked Questions

How long did the split take?

The actual migration took about six months, but the team spent another three months stabilizing the new architecture. The first two months were spent on identifying contexts and designing the APIs; the next three months were a gradual strangler-fig migration, moving one feature at a time. The final month was dedicated to deleting the old monolith code and cleaning up data migrations.

Did the split reduce deployment time?

Yes, but not as much as expected. The average deployment time per service went from 20 minutes to 5 minutes, but the team now deploys 16 services, so the total time for a full release is similar. The benefit is that most changes only require deploying one or two services, so the median time to get a feature to production dropped from a day to a few hours.

What happened to the monolith?

It was deleted. After all features were migrated, the team kept the monolith running for two weeks as a rollback option, then shut it down. The old database was also decommissioned after a data migration script verified that all records were copied to the new per-service databases.

How do you handle data consistency across services?

We use the outbox pattern for all writes that need to trigger actions in other services. Each service writes an event to its own outbox table as part of the same database transaction as the business data. A background worker reads the outbox and publishes the event to a message broker. Consumers retry on failure and use idempotency keys to avoid duplicate processing.

Is 16 contexts too many?

For this team and product, it felt right. Each context is small enough that a new developer can understand it in a day. But if we had fewer than 10 developers, 16 would be too many. The right number depends on team size, feature velocity, and the team’s experience with distributed systems.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.