Microfrontends
Build large applications as independent, self-contained modules that can be developed and maintained by separate teams.
Overview
GoRouter Modular's module system naturally supports a microfrontend architecture. Each module encapsulates its own routes, dependencies, and business logic — making it possible for multiple teams to work on the same app independently.
Architecture
Each module is a self-contained unit with:
- Routes — Pages and navigation within the module
- Dependencies — Services and controllers via dependency injection
- Business logic — Isolated from other modules
- Events — Cross-module communication via
EventModule
class AppModule extends Module {
@override
List<ModularRoute> get routes => [
ModuleRoute('/shop', module: ShopModule()), // Team A
ModuleRoute('/users', module: UsersModule()), // Team B
ModuleRoute('/analytics', module: AnalyticsModule()), // Team C
ModuleRoute('/admin', module: AdminModule()), // Team D
];
}Module Communication
Modules communicate through events, avoiding direct dependencies:
// Shop module fires an event
ModularEvent.fire(ProductAddedToCartEvent(productId: '123'));
// Analytics module listens
class AnalyticsModule extends EventModule {
@override
void listen() {
on<ProductAddedToCartEvent>((event, context) {
trackEvent('product_added', event.productId);
});
}
}See Event Module for full documentation.
Team Structure Example
Team A: Product Catalog
class ShopModule extends Module {
@override
FutureBinds binds(Injector i) {
i.addSingleton<ProductService>((i) => ProductService());
}
@override
List<ModularRoute> get routes => [
ChildRoute('/', child: (_, __) => ProductListPage()),
ChildRoute('/product/:id', child: (_, state) =>
ProductDetailPage(id: state.pathParameters['id']!)),
ChildRoute('/cart', child: (_, __) => CartPage()),
];
}Team B: User Management
class UsersModule extends Module {
@override
FutureBinds binds(Injector i) {
i.addSingleton<UserService>((i) => UserService());
}
@override
List<ModularRoute> get routes => [
ChildRoute('/', child: (_, __) => UserListPage()),
ChildRoute('/profile/:id', child: (_, state) =>
UserProfilePage(id: state.pathParameters['id']!)),
];
}Dependency Isolation
Each module manages its own dependencies. Binds are registered when the module is first visited and disposed when the module exits the navigation tree:
class OrdersModule extends Module {
@override
FutureBinds binds(Injector i) {
i.addSingleton<OrderService>((i) => OrderService());
i.addSingleton<PaymentService>((i) => PaymentService());
}
@override
List<ModularRoute> get routes => [
ChildRoute('/', child: (_, __) => OrdersPage()),
ChildRoute('/:id', child: (_, state) =>
OrderDetailPage(id: state.pathParameters['id']!)),
];
}No module can accidentally access another module's internal services unless explicitly shared through the parent module.
When to Use
| Scenario | Microfrontends? |
|---|---|
| Large teams (10+ developers) | Yes |
| Complex business domains | Yes |
| Features owned by different teams | Yes |
| Small team, simple app | No — standard modules are sufficient |
| Tightly coupled features | No — keep in the same module |
Best Practices
- Define clear module boundaries — Each module should own a distinct feature or domain.
- Communicate via events — Avoid direct dependencies between modules.
- Share UI through a design system — Keep a shared theme/widget library outside the module layer.
- Test modules independently — Each module should be testable in isolation.
- Use
ModuleRoutefor every team boundary — This ensures proper DI isolation and auto-dispose.