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.
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.
GraphQL federation as the enabler
Section titled “GraphQL federation as the enabler”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.
Where the SaaS integrations sit
Section titled “Where the SaaS integrations sit”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.
Local development: the monolith
Section titled “Local development: the monolith”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.
Choosing a topology
Section titled “Choosing a topology”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.

