Skip to content

New: AI agent integration via Model Context Protocol (MCP).Learn more

Composition & deployment topologies

The central idea behind the Evolve framework is that functionality and deployment are decoupled. Features ship as modules — self-contained bundles of GraphQL schema, resolvers, and HTTP routes published in the @evolve-framework/* packages. How many services you run is a separate decision, made in your implementation, per environment, and revisable at any time.

This is what makes Evolve work for a two-person team and a twenty-person organization alike: both compose the same modules, they just deploy them differently.

The module is the unit, the service is a choice

Section titled “The module is the unit, the service is a choice”

A module (CatalogModule, CheckoutModule, ContentfulModule, StripePaymentModule, …) carries everything one capability needs. A service is nothing more than a thin host that mounts one or more modules through createDomainService. That gives you a spectrum:

One backend service. Compose every module you use into a single CompositeModule and run one deployable. CompositeModule merges the modules’ schemas and resolvers into one subgraph, concatenates their HTTP routes, and chains their lifecycles. Cross-module references keep working: when one module hands another an entity stub (a CMS page referencing a ProductVariant by SKU, say), the composite hydrates it through the owning module’s reference resolver — the same mechanic a federation gateway applies between subgraphs, applied inside a single service.

One service per domain. The platform’s default layout: catalog, checkout, account, orders, quotes, and CMS each run as their own service and join the supergraph as separate subgraphs. Each can scale, deploy, and fail independently — and each can be owned by a different team.

Anything in between. Because both composition mechanisms produce valid federation subgraphs, you can start with one service and peel domains off as your team or traffic grows — or fold services back together to cut infrastructure cost. The storefront never notices: it talks to one federated graph either way.

A single service hosting three domains
export const createModule = (): CompositeModule => {
return new CompositeModule([
new CatalogModule({ clientFactory }),
new CheckoutModule({ config }),
new ContentfulModule({ config: contentfulConfig }),
]);
};

Moving ContentfulModule out of this composite and into its own service is a topology change, not a code change: the module is instantiated the same way in either host.

Federation is what makes the topology invisible to consumers. The storefront queries one schema through the gateway; whether a field is resolved by a subgraph running alone or by a module inside a composite is a routing detail. Entity references (@key types like Product, Customer, BusinessUnit) resolve across whatever boundaries happen to exist — between subgraphs via the gateway’s query planner, and between modules inside one service via the composite’s entity hydration.

This also means the schema contract is stable while the deployment evolves: splitting a service does not break clients, because the supergraph they see is recomposed, not redesigned.

Vendor integrations are deliberately packaged below the topology decision. @evolve-framework/commercetools owns the commerce domain modules, @evolve-framework/contentful and @evolve-framework/storyblok own CMS modules, and the commercetools-* payment packages own PSP integrations. Your services import and configure them; they never contain vendor logic themselves.

The practical consequences:

  • Swapping a vendor is swapping a module. The platform ships two interchangeable CMS slots (Contentful or Storyblok) — the service code around them is identical.
  • Adding a vendor doesn’t touch the topology. A new PSP is a new module mounted in an existing (or new) service — see Adding a payment provider.
  • Project-specific code stays in your project. Custom modules compose alongside framework modules in the same CompositeModule — see Customization.

The platform’s dev setup exercises this flexibility daily: pnpm dev boots every service module inside a single process (the monolith runner, one worker thread per module) while production runs them as separate containers. Same modules, different topology — which is exactly the point.

Some rules of thumb:

Situation Lean toward
Small team, early project Fewer services — less operational surface
Independent team ownership per domain Service per domain
One domain dominates load (e.g. catalog) Split that domain out, keep the rest together
Strict blast-radius or compliance boundaries Separate services at the boundary
Local development The monolith runner, always

Whatever you choose, the schema your consumers see — and the module code you maintain — stays the same.