Micro‑Federation
Micro-federation allows you to compose multiple Axolotl modules into a single GraphQL API. This architectural pattern is ideal for organizing large applications into logical domains while maintaining a unified API surface.
What is Micro-Federation?
Micro-federation in Axolotl is a modular approach where:
- Each domain (e.g., users, todos, products) has its own GraphQL schema and resolvers
- Schemas are automatically merged into a single supergraph at build time
- Each module maintains its own type safety with generated models
- Resolvers are intelligently merged at runtime to handle overlapping types
Unlike Apollo Federation, Axolotl's micro-federation is designed for monorepo or single-project architectures where all modules are built and deployed together.
When to Use Micro-Federation
Good use cases:
- Large monorepo applications with distinct domain modules
- Teams working on separate features within the same codebase
- Projects where you want to organize GraphQL code by business domain
- Applications that need to scale code organization without microservices complexity
Not recommended for:
- Distributed services that deploy independently (use Apollo Federation instead)
- Simple applications with only a few types and resolvers
- Projects where all types are tightly coupled
Quick Start
1. Configure axolotl.json
{
"schema": "schema.graphql",
"models": "src/models.ts",
"federation": [
{
"schema": "src/todos/schema.graphql",
"models": "src/todos/models.ts"
},
{
"schema": "src/users/schema.graphql",
"models": "src/users/models.ts"
}
]
}2. Create Submodule Schemas
Each module defines its own schema:
type User {
_id: String!
username: String!
}
type Mutation {
login(username: String!, password: String!): String! @resolver
register(username: String!, password: String!): String! @resolver
}
type Query {
user: AuthorizedUserQuery! @resolver
}
type AuthorizedUserQuery {
me: User! @resolver
}
directive @resolver on FIELD_DEFINITION
schema {
query: Query
mutation: Mutation
}type Todo {
_id: String!
content: String!
done: Boolean
}
type AuthorizedUserMutation {
createTodo(content: String!): String! @resolver
todoOps(_id: String!): TodoOps! @resolver
}
type AuthorizedUserQuery {
todos: [Todo!] @resolver
todo(_id: String!): Todo! @resolver
}
type TodoOps {
markDone: Boolean @resolver
}
directive @resolver on FIELD_DEFINITION
type Query {
user: AuthorizedUserQuery @resolver
}
type Mutation {
user: AuthorizedUserMutation @resolver
}
schema {
query: Query
mutation: Mutation
}3. Run Code Generation
axolotl buildThis command:
- Generates models for each submodule (
src/todos/models.ts,src/users/models.ts) - Merges all submodule schemas into the supergraph (
schema.graphql) - Generates models for the supergraph (
src/models.ts)
4. Create Axolotl Instances per Module
Each module needs its own axolotl.ts file to create type-safe resolver helpers:
import { Models } from '@/src/users/models.js';
import { Axolotl } from '@aexol/axolotl-core';
import { graphqlYogaAdapter } from '@aexol/axolotl-graphql-yoga';
export const { createResolvers, createDirectives, applyMiddleware } = Axolotl(graphqlYogaAdapter)<Models, unknown>();import { Models } from '@/src/todos/models.js';
import { Axolotl } from '@aexol/axolotl-core';
import { graphqlYogaAdapter } from '@aexol/axolotl-graphql-yoga';
export const { createResolvers, createDirectives, applyMiddleware } = Axolotl(graphqlYogaAdapter)<Models, unknown>();5. Implement Resolvers in Each Module
import { createResolvers } from '../axolotl.js';
import Mutation from './Mutation/resolvers.js';
import Query from './Query/resolvers.js';
import AuthorizedUserQuery from './AuthorizedUserQuery/resolvers.js';
export default createResolvers({
...Mutation,
...Query,
...AuthorizedUserQuery,
});import { createResolvers } from '../../axolotl.js';
import { db } from '../../db.js';
export default createResolvers({
Mutation: {
login: async (_, { password, username }) => {
const user = db.users.find((u) => u.username === username && u.password === password);
return user?.token;
},
},
});import { createResolvers } from '../axolotl.js';
import AuthorizedUserQuery from './AuthorizedUserQuery/resolvers.js';
import AuthorizedUserMutation from './AuthorizedUserMutation/resolvers.js';
import TodoOps from './TodoOps/resolvers.js';
export default createResolvers({
...AuthorizedUserQuery,
...AuthorizedUserMutation,
...TodoOps,
});6. Merge Resolvers in Main File
import { mergeAxolotls } from '@aexol/axolotl-core';
import todosResolvers from '@/src/todos/resolvers/resolvers.js';
import usersResolvers from '@/src/users/resolvers/resolvers.js';
export default mergeAxolotls(todosResolvers, usersResolvers);7. Use in Your Server
import { adapter } from '@/src/axolotl.js';
import resolvers from '@/src/resolvers.js';
adapter({ resolvers }).server.listen(4000, () => {
console.log('Server listening on port 4000');
});How It Works
Schema Merging
When you run axolotl build, schemas are merged using the following rules:
Types are merged by name:
- If
Usertype exists in multiple schemas, all fields are combined - Fields with the same name must have identical type signatures
- If there's a conflict, the build fails with a detailed error
Example - Types get merged:
# users/schema.graphql
type User {
_id: String!
username: String!
}
# todos/schema.graphql
type User {
_id: String!
}
# Merged result in schema.graphql
type User {
_id: String!
username: String! # Field from users module
}Root types (Query, Mutation, Subscription) are automatically merged:
# users/schema.graphql
type Query {
user: AuthorizedUserQuery! @resolver
}
# todos/schema.graphql
type Query {
user: AuthorizedUserQuery @resolver
}
# Merged result - fields combined
type Query {
user: AuthorizedUserQuery @resolver
}Resolver Merging
The mergeAxolotls function intelligently merges resolvers:
1. Non-overlapping resolvers are combined:
// users: { Mutation: { login: fn1 } }
// todos: { Mutation: { createTodo: fn2 } }
// Result: { Mutation: { login: fn1, createTodo: fn2 } }2. Overlapping resolvers are executed in parallel and results are deep-merged:
// users: { Query: { user: () => ({ username: "john" }) } }
// todos: { Query: { user: () => ({ todos: [...] }) } }
// Result: { Query: { user: () => ({ username: "john", todos: [...] }) } }This allows multiple modules to contribute different fields to the same resolver!
3. Subscriptions cannot be merged - only the first one is used:
// If multiple modules define the same subscription, only the first is used
// This is because subscriptions have a single event streamType Generation Flow
1. Read axolotl.json
↓
2. For each federation entry:
- Parse schema file
- Generate models.ts with TypeScript types
↓
3. Merge all schemas using graphql-js-tree
↓
4. Write merged schema to root schema file
↓
5. Generate root models.ts from supergraph
↓
6. Each module uses its own models for type safetyDirectory Structure
Here's a recommended structure for a federated project:
project/
├── axolotl.json # Main config with federation array
├── schema.graphql # Generated supergraph (don't edit manually)
├── src/
│ ├── models.ts # Generated supergraph models
│ ├── axolotl.ts # Main Axolotl instance
│ ├── resolvers.ts # Merged resolvers (calls mergeAxolotls)
│ ├── index.ts # Server entry point
│ │
│ ├── users/ # Users domain module
│ │ ├── schema.graphql # Users schema
│ │ ├── models.ts # Generated from users schema
│ │ ├── axolotl.ts # Users Axolotl instance
│ │ ├── db.ts # Users data layer
│ │ └── resolvers/
│ │ ├── resolvers.ts # Main users resolvers export
│ │ ├── Mutation/
│ │ │ ├── resolvers.ts
│ │ │ ├── login.ts
│ │ │ └── register.ts
│ │ └── Query/
│ │ ├── resolvers.ts
│ │ └── user.ts
│ │
│ └── todos/ # Todos domain module
│ ├── schema.graphql
│ ├── models.ts
│ ├── axolotl.ts
│ ├── db.ts
│ └── resolvers/
│ ├── resolvers.ts
│ ├── AuthorizedUserMutation/
│ ├── AuthorizedUserQuery/
│ └── TodoOps/Advanced Topics
Sharing Types Across Modules
When modules need to reference the same types, define them in each schema:
type User {
_id: String!
username: String!
}type User {
_id: String! # Shared fields must match exactly
}
type Todo {
_id: String!
content: String!
owner: User! # Reference the shared type
}The schemas will be merged, and the User type will contain all fields from both definitions.
Cross-Module Dependencies
Modules can extend each other's types by defining resolvers for shared types:
import { createResolvers } from '@/src/axolotl.js';
import { db as usersDb } from '@/src/users/db.js';
// Todos module contributes to the Query.user resolver
export default createResolvers({
Query: {
user: async (input) => {
const token = input[2].request.headers.get('token');
const user = usersDb.users.find((u) => u.token === token);
if (!user) throw new Error('Not authorized');
return user;
},
},
});When multiple modules implement the same resolver, their results are deep-merged automatically.
Custom Scalars in Federation
Define scalars in each module that uses them:
scalar Secret
type AuthorizedUserMutation {
createTodo(content: String!, secret: Secret): String! @resolver
}Scalar resolvers should be defined once in the main axolotl.ts:
import { Axolotl } from '@aexol/axolotl-core';
import { Models } from '@/src/models.js';
export const { adapter, createResolvers } = Axolotl(graphqlYogaAdapter)<
Models<{ Secret: number }>, // Map custom scalar to TypeScript type
unknown
>();Subscriptions in Federation
Subscriptions work in federated setups, but each subscription field should only be defined in one module:
import { createResolvers } from '../../axolotl.js';
export default createResolvers({
Subscription: {
countdown: {
subscribe: async function* (_, { from }) {
for (let i = from ?? 3; i >= 0; i--) {
yield { countdown: i };
await new Promise((resolve) => setTimeout(resolve, 1000));
}
},
},
},
});If multiple modules try to define the same subscription field, only the first one encountered will be used.
Directives Across Modules
Directives must be defined in each schema that uses them:
directive @resolver on FIELD_DEFINITION
directive @auth(role: String!) on FIELD_DEFINITIONDirective implementations should be registered in each module's Axolotl instance that needs them.
Development Workflow
Initial Setup
# Install dependencies
npm install
# Generate all models and merged schema
npm run models # or: axolotl build
# Generate resolver scaffolding (optional)
npm run resolvers # or: axolotl resolversDevelopment Mode
# Watch for changes and rebuild
npm run watch
# In another terminal, run the dev server
npm run devWhen to Regenerate
Run axolotl build when you:
- Add or modify any schema file
- Add new types or fields
- Add or remove federation modules
- Change
axolotl.jsonconfiguration
The CLI will regenerate:
- Each submodule's
models.ts - The merged
schema.graphql - The root
models.ts
Production Build
# Build TypeScript
npm run build
# Start production server
npm run startBest Practices
Module Organization
Do:
- Organize by business domain (users, products, orders)
- Keep related types in the same module
- Use consistent directory structure across modules
- Make modules as independent as possible
Don't:
- Create modules for every single type
- Mix unrelated concerns in one module
- Create circular dependencies between modules
Naming Conventions
Shared types: Use consistent names across modules
# Good: Both modules use "User"
type User {
_id: String!
}
# Bad: Different names for same concept
type UserAccount {
_id: String!
}
type UserProfile {
_id: String!
}Module-specific types: Prefix with domain
# Good
type TodoItem { ... }
type TodoFilter { ... }
# Also acceptable
type Todo { ... }
type TodosFilter { ... }Resolver Organization
Group resolvers by type and field:
resolvers/
├── resolvers.ts # Main export with createResolvers
├── Query/
│ ├── resolvers.ts # Combines all Query resolvers
│ ├── user.ts
│ └── users.ts
├── Mutation/
│ ├── resolvers.ts
│ ├── createUser.ts
│ └── updateUser.ts
└── User/ # Type resolvers
├── resolvers.ts
└── posts.tsTesting Strategy
Test modules independently:
import { createResolvers } from '@/src/users/axolotl.js';
import usersResolvers from '@/src/users/resolvers/resolvers.js';
describe('Users Module', () => {
it('should login user', async () => {
const result = await usersResolvers.Mutation.login({}, { username: 'test', password: 'pass' }, mockContext);
expect(result).toBeDefined();
});
});Test the merged supergraph:
import mergedResolvers from '@/src/resolvers.js';
describe('Supergraph', () => {
it('should merge user data from multiple modules', async () => {
const result = await mergedResolvers.Query.user({}, {}, mockContext);
expect(result.username).toBeDefined(); // from users module
expect(result.todos).toBeDefined(); // from todos module
});
});Performance Considerations
Parallel resolver execution: When multiple modules implement the same resolver, they execute in parallel using Promise.all(). This is efficient but be aware of:
- Database connection limits
- Rate limiting for external APIs
- Memory usage with many concurrent operations
Deep merge overhead: Results are deep-merged using object spreading. For very large response objects, this may add latency. Consider:
- Keeping resolver return values focused
- Avoiding deeply nested objects when possible
- Using DataLoader for batching database queries
Troubleshooting
Schema Merge Conflicts
Error: Federation conflict on Node.field pattern: User.email
Cause: The same field on the same type has different definitions across modules.
Solution: Ensure field types match exactly:
# users/schema.graphql
type User {
email: String! # Required
}
# profile/schema.graphql
type User {
email: String # Optional - CONFLICT!
}Fix by making them identical:
# Both files
type User {
email: String!
}Type Generation Failures
Error: Cannot find module '@/src/users/models.js'
Cause: Models haven't been generated yet or paths are incorrect.
Solution:
# Regenerate all models
axolotl build
# Check your tsconfig.json has correct path mappings
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}Resolver Not Found
Error: Resolver returns null or is not called
Cause:
- Resolver not exported from module
- Not included in
mergeAxolotlscall - Schema and resolver names don't match
Solution:
// Ensure resolver is exported
export default createResolvers({
Query: {
user: async () => { ... } // Must match schema field name exactly
}
});
// Ensure it's merged
import usersResolvers from './users/resolvers/resolvers.js';
export default mergeAxolotls(usersResolvers, ...otherResolvers);Merged Resolver Returns Unexpected Data
Issue: Merged resolver combines data incorrectly
Cause: Deep merge combines objects in unexpected ways
Solution: Ensure resolvers return compatible object shapes:
// Module 1
{ Query: { user: () => ({ id: "1", name: "John" }) } }
// Module 2
{ Query: { user: () => ({ id: "1", posts: [...] }) } }
// Merged result - objects combined
{ id: "1", name: "John", posts: [...] }If modules return conflicting primitives, the last one wins. Use different field names to avoid conflicts.
Subscription Not Working
Issue: Subscription not firing or using wrong implementation
Cause: Multiple modules define the same subscription
Solution: Define each subscription in only one module. If you need different subscription behavior, use different field names:
# users/schema.graphql
type Subscription {
userUpdated: User!
}
# todos/schema.graphql
type Subscription {
todoUpdated: Todo!
}Running the Example
The repository includes a complete federated example you can run:
# Navigate to the example
cd examples/yoga-federated
# Install dependencies (if not already done at root)
npm install
# Generate models
npm run models
# Run in development mode
npm run devVisit http://localhost:4002/graphql and try these operations:
# Register a user
mutation Register {
register(username: "user", password: "password")
}
# Login (returns token)
mutation Login {
login(username: "user", password: "password")
}
# Set the token in headers: { "token": "your-token-here" }
# Create a todo
mutation CreateTodo {
user {
createTodo(content: "Learn Axolotl Federation")
}
}
# Query merged data (comes from both users and todos modules!)
query MyData {
user {
me {
_id
username
}
todos {
_id
content
done
}
}
}Comparison with Apollo Federation
| Feature | Axolotl Micro-Federation | Apollo Federation |
|---|---|---|
| Deployment | Single service | Multiple services |
| Schema Merging | Build-time | Runtime with gateway |
| Type Splitting | Automatic deep merge | Explicit @key directives |
| Resolver Execution | Parallel within process | Cross-service HTTP calls |
| Performance | Fast (in-process) | Network overhead |
| Complexity | Simple config | Gateway + federation service |
| Use Case | Monorepo/single app | Distributed microservices |
| Independence | Shared codebase | Fully independent services |
Next Steps
- Review the complete example (opens in a new tab)
- Learn about Data Loaders to optimize cross-module queries
- Explore Directives for cross-cutting concerns
- Check out Best Practices for scaling your federated architecture