Micro Frontends
Overview

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

ScenarioMicrofrontends?
Large teams (10+ developers)Yes
Complex business domainsYes
Features owned by different teamsYes
Small team, simple appNo — standard modules are sufficient
Tightly coupled featuresNo — keep in the same module

Best Practices

  1. Define clear module boundaries — Each module should own a distinct feature or domain.
  2. Communicate via events — Avoid direct dependencies between modules.
  3. Share UI through a design system — Keep a shared theme/widget library outside the module layer.
  4. Test modules independently — Each module should be testable in isolation.
  5. Use ModuleRoute for every team boundary — This ensures proper DI isolation and auto-dispose.