Architecture
The Evolve Framework is an ongoing, backwards-compatible refactoring. Not all services have been migrated yet, and APIs may still evolve. Everything described here is functional but should be considered pre-release.
The framework organizes shared backend code into three layered packages. Services compose GraphQL modules from these packages and add only project-specific logic.
Three-layer design
- Core (
@evolve-framework/core): theAbstractGraphQLModulebase class,DomainService,ProcessManager, cache handler, and shared infrastructure. Vendor-agnostic. - Schemas (
@evolve-framework/schemas): all GraphQL type definitions, organized by domain (cart, catalog, customer, order, and so on). Vendor-agnostic. - Implementation package: the concrete implementation for a specific SAAS integration. Contains resolver implementations, data mappers, dataloaders, and composable GraphQL modules that wire everything together.
The core and schemas layers are vendor-agnostic. Each SAAS integration gets
its own implementation package that builds on the shared foundation. The
first implementation package is @evolve-framework/commercetools. Additional
packages for other integrations (Contentful, Storyblok, Algolia, Shopify, and others) will follow the same pattern, sharing the same core and schemas.
GraphQL module system
Each domain is encapsulated in a module that extends
AbstractGraphQLModule. A module bundles the GraphQL type definitions and
resolver implementations for its domain.
import { AbstractGraphQLModule } from "@evolve-framework/core";
import typeDefs from "@evolve-framework/schemas/graphql/cart";
export class CartGraphQLModule extends AbstractGraphQLModule {
resolvers: Resolvers = {
Query: {
cart: cartResolver,
},
Mutation: {
cartLineItemsAdd: cartLineItemsAddMutation,
cartLineItemsUpdate: cartLineItemsUpdateMutation,
cartLineItemsRemove: cartLineItemsRemoveMutation,
cartUpdate: cartUpdateMutation,
cartDiscountCodeAdd: cartDiscountCodeAdd,
cartDiscountCodeRemove: cartDiscountCodeRemove,
},
Cart: {
availablePaymentMethods: availablePaymentMethodsResolver,
availableShippingMethods: availableShippingMethodsResolver,
discountCodes: discountCodesResolver,
lineItems: lineItemsResolver,
selectedShippingMethod: selectedShippingMethodResolver,
shippingCosts: shippingCostsResolver,
},
};
typedefs = typeDefs;
}
Module composition
GraphQLCompositeModule composes multiple modules into a single set of
type definitions and resolvers. It handles merging schemas (with conflict
detection) and merging resolvers by GraphQL type name.
import {
CheckoutGraphQLModule,
CartGraphQLModule,
GraphQLCompositeModule,
} from "@evolve-framework/commercetools";
const module = new GraphQLCompositeModule([
new CheckoutGraphQLModule(),
new CartGraphQLModule(),
]);
const typeDefs = module.getTypedefs(); // Merged schema
const resolvers = module.getResolvers(); // Merged resolvers
Module configuration
Some modules require configuration (for example, which search engine to use or where payment services are located). Pass config through the constructor:
const module = new GraphQLCompositeModule([
new CatalogGraphQLModule({
config: {
searchEngine: "algolia",
algoliaAppId: config.ALGOLIA_APP_ID,
algoliaSearchApiKey: config.ALGOLIA_SEARCH_API_KEY,
},
}),
new CartGraphQLModule({
config: {
paymentServiceEndpoints: config.PAYMENT_SERVICE_ENDPOINTS,
},
}),
]);
GraphQLCompositeModule aggregates all module configs. You retrieve the
merged config with module.getConfig() and pass it into the context
factory so resolvers can access it as context.config.
Available modules (commercetools)
The @evolve-framework/commercetools package ships with the following
modules. Other implementation packages will provide modules for their
respective domains.
| Module | Domain | Key operations |
|---|---|---|
CustomerGraphQLModule | Authentication, customer management | Login, registration, address CRUD, password management |
CartGraphQLModule | Shopping cart | Line item CRUD, discount codes, cart updates |
CatalogGraphQLModule | Product catalog | Product search, categories, product details |
CheckoutGraphQLModule | Checkout | Checkout completion |
OrderGraphQLModule | Orders | Order queries, order line items |
PaymentGraphQLModule | Payments | Payment creation, Apple Pay validation |
BusinessUnitGraphQLModule | B2B | Business unit management, associates, roles |
QuoteGraphQLModule | Quotes | Quote requests, quote management |
ShoppingListGraphQLModule | Shopping lists | List CRUD, item management |
CommonGraphQLModule | Shared types | Common scalars and base types |
Centralized infrastructure
The @evolve-framework/core package provides infrastructure shared by all
services regardless of backend:
DomainService: the HTTP and GraphQL server that runs your serviceProcessManager: process lifecycle with graceful shutdown on SIGTERM/SIGINT- Cache: Redis-backed caching with
cache.configure(),cache.wrapFn(), and automatic fallback when Redis is unavailable - HTTP utilities: traced fetch client, retry with exponential backoff, URL joining
- Error helpers:
assertAuthenticated(),validateZodSchema(), and other utilities for writing resolvers
Each implementation package bundles additional infrastructure specific to
its backend. The @evolve-framework/commercetools package provides:
- DataLoaders: 20+ batching loaders (products, categories, customers,
orders, and more) via
createDataLoaders() - Client context:
CommercetoolsClientLoaderfor direct auth,RemoteClientContextLoaderfor federated auth - Token management:
configureTokenManager()for OAuth token lifecycle - Client factory:
configureClientFactory()for commercetools API clients - Mappers: data transformation functions for addresses, customers, products, carts, and other domains
- Validation: Zod schemas for address validation, phone number parsing, and more
- Search: Algolia and commercetools native search abstractions
- Test utilities: shared test server, mock data, and context fixtures
Other implementation packages will provide equivalent infrastructure for their respective integrations.
Test factories
The @evolve-framework/commercetools package also includes 20+ test
factories (built on fishery) for
commercetools resources: carts, customers, products, categories, orders,
payments, business units, and more. Use these in your service tests to
generate realistic mock data without boilerplate.
The service pattern
Every migrated service follows the same minimal structure:
| File | Purpose | What varies per service |
|---|---|---|
index.ts | Entry point: start the process via ProcessManager | Identical across services |
init.ts | Configure framework infrastructure | Which functions to call (account needs configureTokenManager, catalog does not) |
server.ts | Compose modules into a DomainService | Which modules, plugins, and REST routes to include |
context.ts | Build the per-request GraphQL context | Direct auth vs. federated auth |
Entry point and lifecycle
Every service uses ProcessManager from @evolve-framework/core as its
entry point. It handles startup, graceful shutdown on SIGTERM/SIGINT, and
error handling:
// index.ts
import { ProcessManager } from "@evolve-framework/core";
import { cache } from "@evolve-framework/core/cache";
let app: DomainService;
const pm = new ProcessManager({
start: async () => {
await initEnvironment();
app = createApp();
await app.start();
},
stop: async () => {
await app?.stop();
await cache.close();
},
});
await pm.start();
DomainService provides start() and stop() methods to control the
HTTP and GraphQL server.
Example: order service
The order service is the simplest case (single module, no cache, federated auth):
// init.ts
import {
configureClientFactory,
} from "@evolve-framework/commercetools";
export const initEnvironment = async () => {
await loadConfig();
await configureClientFactory(config);
};
// server.ts
import {
OrderGraphQLModule,
GraphQLCompositeModule,
} from "@evolve-framework/commercetools";
import { DomainService } from "@evolve-framework/core";
const module = new GraphQLCompositeModule([
new OrderGraphQLModule(),
]);
export const createApp = () =>
new DomainService({
name: config.COMPONENT_NAME,
graphql: {
typeDefs: module.getTypedefs(),
resolvers: module.getResolvers(),
context: newContext,
plugins: [useClientContext()],
},
http: {
address: { host: config.HTTP_HOST, port: config.HTTP_PORT },
},
});
Example: checkout service
The checkout service composes two modules and exposes a service-specific REST endpoint:
// server.ts
import {
CheckoutGraphQLModule,
CartGraphQLModule,
GraphQLCompositeModule,
} from "@evolve-framework/commercetools";
import { DomainService } from "@evolve-framework/core";
const module = new GraphQLCompositeModule([
new CheckoutGraphQLModule(),
new CartGraphQLModule(),
]);
export const createApp = () =>
new DomainService({
name: config.COMPONENT_NAME,
graphql: {
typeDefs: module.getTypedefs(),
resolvers: module.getResolvers(),
context: newContext,
},
http: {
address: { host: config.HTTP_HOST, port: config.HTTP_PORT },
routes: (app) => {
app.post("/api/extension", {
handler: extensionHandler,
});
},
},
});
Plugins
DomainService accepts GraphQL plugins in the graphql.plugins array.
The framework and supporting packages provide several:
useClientContext(): reads the access token from the request header and loads the client context. Used by every service.useFederatedToken(): manages federated token lifecycle. Only needed in the account service (which issues tokens directly).useRateLimiter(): rate-limits GraphQL operations. Useful for endpoints exposed to unauthenticated traffic.
new DomainService({
name: config.COMPONENT_NAME,
graphql: {
// ...
plugins: [
useRateLimiter({ /* ... */ }),
useFederatedToken(),
useClientContext(),
],
},
});
Most services only need useClientContext(). Add the others when your
service requires them.
Context and store context
The context.ts file builds the per-request GraphQL context that resolvers
receive. Two key pieces are the store context and the dataloaders.
Store context
StoreContext (from @evolve-framework/core) reads multi-store headers
from each request: X-StoreContext-Currency, X-StoreContext-Locale, and
X-StoreContext-StoreKey. The GraphQL gateway sets these headers based on
the incoming request.
import { readStoreContextFromRequest } from "@evolve-framework/core";
const storeContext = readStoreContextFromRequest(request);
Dataloaders
There are two patterns for initializing dataloaders, depending on the service's authentication model:
Using initContextValue (most services): the framework initializes all
dataloaders using the global-scoped client. Use this when the service
delegates authentication to the account service.
import {
initContextValue,
RemoteClientContextLoader,
} from "@evolve-framework/commercetools";
export const newContext = async ({ request }) => {
const storeContext = readStoreContextFromRequest(request);
const loader = new RemoteClientContextLoader(
config.ACCOUNT_SERVICE_ENDPOINT,
clientFactory,
);
const clientContext = new ClientContext(storeContext, loader);
const context = { storeContext, clientContext };
initContextValue(context);
return context;
};
Manual dataloader creation (account service): the service creates dataloaders explicitly because it manages authentication directly.
import {
createDataLoaders,
CommercetoolsClientLoader,
} from "@evolve-framework/commercetools";
export const newContext = async ({ request }) => {
const storeContext = readStoreContextFromRequest(request);
const loader = new CommercetoolsClientLoader(clientFactory);
const clientContext = new ClientContext(storeContext, loader);
return {
storeContext,
clientContext,
loaders: createDataLoaders(/* ... */),
};
};
Cache
The @evolve-framework/core/cache export provides a Redis-backed cache
singleton. Configure it during initialization and close it on shutdown:
import { cache } from "@evolve-framework/core/cache";
// In init.ts
await cache.configure(config.REDIS_URL);
// In your code
const result = await cache.wrapFn({
key: "my-cache-key",
fn: () => fetchExpensiveData(),
ttl: 60, // seconds
});
// In stop handler
await cache.close();
The cache degrades gracefully: if Redis is unavailable, it falls back to a no-op store so the service continues to function without caching.
Request flow
What moved where
| Category | Previously in each service | New location |
|---|---|---|
| Resolvers | src/resolvers/ | Implementation package |
| Schema definitions | inline GraphQL strings | @evolve-framework/schemas |
| DataLoaders | src/dataloaders/ | Implementation package |
| Mappers | src/mappers/ | Implementation package |
| Core logic | src/core/ | @evolve-framework/core |
| Validation | Zod schemas, error types | Implementation package |
| Test utilities | src/testing/ | Implementation package |
| Types | src/types.ts | Implementation package |
What stays in your service
Not everything moves into the framework. Your service still owns:
- Service configuration:
init.tswith environment-specific setup - Context wiring:
context.tswith your auth pattern - REST endpoints: API extension handlers, webhooks, custom routes
- Event handlers: service-specific event processing
- Jobs: scheduled tasks and background workers
- Project-specific resolvers: any custom or overridden resolvers