Lesson 07-Micro Frontend Architecture Design

Micro Frontend Decomposition Strategies

Decomposition by Business Domain

Core Concept: Divide micro frontend applications based on business domain boundaries, with each application responsible for a specific business domain.

Implementation Steps:

  1. Business Domain Analysis: Identify the main business domains in the system (e.g., user management, order management, payment management).
  2. Domain Boundary Definition: Clearly define the responsibilities and interaction points of each business domain.
  3. Application Division: Split each business domain into an independent micro frontend application.

Example:

- User Management Micro Frontend (user-management)
  - User Registration
  - User Login
  - User Profile Management
- Order Management Micro Frontend (order-management)
  - Order List
  - Order Details
  - Order Creation
- Payment Management Micro Frontend (payment-management)
  - Payment Methods
  - Payment Records

Advantages:

  • Clear business isolation
  • Facilitates independent maintenance by business teams
  • Aligns with Domain-Driven Design (DDD) principles

Disadvantages:

  • Cross-domain interactions may be complex
  • Requires well-designed communication mechanisms

Decomposition by Functional Module

Core Concept: Divide micro frontend applications based on functional features, with each application responsible for a specific functional module.

Implementation Steps:

  1. Function Decomposition: Break down system functions into independent functional modules.
  2. Module Evaluation: Assess the independence and reusability of each functional module.
  3. Application Division: Split independent functional modules into micro frontend applications.

Example:

- Authentication Module (auth)
  - Login
  - Registration
  - Password Recovery
- Product Display Module (product-display)
  - Product List
  - Product Details
- Shopping Cart Module (shopping-cart)
  - Add Item
  - Remove Item
  - Checkout

Advantages:

  • Strong functional reusability
  • Facilitates independent deployment at the functional level
  • Suitable for applications with relatively independent functions

Disadvantages:

  • May result in too many applications
  • Cross-module interactions require additional design

Decomposition by Team Structure

Core Concept: Divide micro frontend applications based on the organization’s team structure, with each team responsible for one or more micro frontend applications.

Implementation Steps:

  1. Team Division: Clearly define the development teams and their responsibilities within the organization.
  2. Capability Matching: Match system functions with team capabilities.
  3. Application Allocation: Assign micro frontend applications to corresponding teams.

Example:

- Frontend Team A
  - User Interface Micro Frontend (user-interface)
    - Navigation Bar
    - Footer
    - Theme Switching
- Frontend Team B
  - Data Visualization Micro Frontend (data-visualization)
    - Chart Display
    - Data Analysis
- Backend Team
  - API Gateway
  - Shared Services

Advantages:

  • Aligns with organizational structure
  • Clear responsibility boundaries
  • Enhances team autonomy

Disadvantages:

  • May lead to technology stack fragmentation
  • Increases cross-team collaboration costs

Architectural Design Principles

Single Responsibility Principle (SRP)

Definition: A module, class, or function should have only one reason to change.

Application Practices:

  1. Micro Frontend Applications: Each micro frontend application should focus on a specific business function or domain.
  2. Component Design: Components should focus on a single function, avoiding becoming “God components.”
  3. Utility Functions: Each utility function should perform a single specific task.

Example:

// Non-SRP compliant - User component handles both display and validation
function UserComponent() {
  const [user, setUser] = useState({});
  
  function validateUser(user) { /* Validation logic */ }
  
  return <div>{user.name}</div>;
}

// SRP compliant - Separate display and validation logic
function UserDisplay({ user }) {
  return <div>{user.name}</div>;
}

function UserValidator(user) {
  return validateUser(user);
}

Advantages:

  • Improves code maintainability
  • Reduces change risks
  • Facilitates testing and reuse

Open/Closed Principle (OCP)

Definition: Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.

Application Practices:

  1. Plugin Architecture: Design a pluggable micro frontend architecture to support functional extensions through plugins.
  2. Configuration-Driven: Achieve functional changes through configuration rather than code modification.
  3. Abstract Interfaces: Define stable interfaces to allow implementation classes to extend functionality.

Example:

// Non-OCP compliant - Requires modifying core code for new features
class App {
  render() {
    // Core rendering logic
    if (featureAEnabled) {
      // Feature A implementation
    }
    if (featureBEnabled) {
      // Feature B implementation
    }
  }
}

// OCP compliant - Extend functionality through plugins
class App {
  constructor(plugins = []) {
    this.plugins = plugins;
  }
  
  render() {
    // Core rendering logic
    this.plugins.forEach(plugin => plugin.render());
  }
}

Advantages:

  • Enhances system extensibility
  • Reduces maintenance costs
  • Supports incremental evolution

Dependency Inversion Principle (DIP)

Definition: High-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.

Application Practices:

  1. Dependency Injection: Manage dependencies between micro frontends through dependency injection.
  2. Interface Abstraction: Define clear interface specifications to achieve decoupling.
  3. Event-Driven: Use event mechanisms to reduce direct dependencies.

Example:

// Non-DIP compliant - High-level module directly depends on low-level module
class AuthService {
  login() { /* Implementation */ }
}

class App {
  constructor() {
    this.authService = new AuthService(); // Direct dependency on concrete implementation
  }
}

// DIP compliant - Decouple through abstraction and dependency injection
interface IAuthService {
  login(): void;
}

class AuthService implements IAuthService {
  login() { /* Implementation */ }
}

class App {
  constructor(authService: IAuthService) {
    this.authService = authService; // Depend on abstraction
  }
}

Advantages:

  • Increases system flexibility
  • Facilitates implementation replacement
  • Supports polymorphism and extension

Design Pattern Applications

Publish-Subscribe Pattern

Definition: Defines a one-to-many dependency between objects, such that when one object’s state changes, all dependent objects are notified and updated automatically.

Application Scenarios:

  1. Micro Frontend Communication: Facilitates communication between main and sub-applications.
  2. Event Bus: Builds a global event system.
  3. State Change Notification: Notifies relevant components of state changes.

Implementation Example:

class EventBus {
  constructor() {
    this.events = {};
  }
  
  on(event, listener) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(listener);
  }
  
  emit(event, ...args) {
    if (this.events[event]) {
      this.events[event].forEach(listener => listener(...args));
    }
  }
}

// Usage example
const eventBus = new EventBus();

// Sub-application subscribes to event
eventBus.on('user-login', (user) => {
  console.log('User logged in:', user);
});

// Main application publishes event
eventBus.emit('user-login', { id: 1, name: 'John' });

Advantages:

  • Loose coupling
  • Supports one-to-many communication
  • Easy to extend

Observer Pattern

Definition: Defines a one-to-many dependency between objects, such that when one object’s state changes, all dependent objects are notified and updated automatically.

Difference from Publish-Subscribe Pattern:

  • In the Observer pattern, observers directly subscribe to the target object.
  • In the Publish-Subscribe pattern, publishers and subscribers are decoupled through an event channel.

Implementation Example:

class Subject {
  constructor() {
    this.observers = [];
  }
  
  addObserver(observer) {
    this.observers.push(observer);
  }
  
  removeObserver(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }
  
  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  update(data) {
    console.log('Received data:', data);
  }
}

// Usage example
const subject = new Subject();
const observer = new Observer();

subject.addObserver(observer);
subject.notify('Hello World'); // Received data: Hello World

Advantages:

  • Clear direct dependency relationships
  • Simple implementation
  • Suitable for tightly coupled scenarios

Strategy Pattern

Definition: Defines a family of algorithms, encapsulates each one, and makes them interchangeable. The strategy pattern allows algorithms to vary independently of the clients that use them.

Application Scenarios:

  1. Dynamic Algorithm Switching: Select different implementations based on conditions.
  2. Behavior Parameterization: Pass behavior as a parameter.
  3. Avoid Conditional Statements: Replace complex if-else or switch-case statements.

Implementation Example:

// Payment strategy interface
class PaymentStrategy {
  pay(amount) {
    throw new Error('Method not implemented');
  }
}

// Concrete payment strategies
class CreditCardPayment extends PaymentStrategy {
  pay(amount) {
    console.log(`Paid ${amount} via Credit Card`);
  }
}

class PayPalPayment extends PaymentStrategy {
  pay(amount) {
    console.log(`Paid ${amount} via PayPal`);
  }
}

// Context class
class PaymentContext {
  constructor(strategy) {
    this.strategy = strategy;
  }
  
  setStrategy(strategy) {
    this.strategy = strategy;
  }
  
  executePayment(amount) {
    this.strategy.pay(amount);
  }
}

// Usage example
const context = new PaymentContext(new CreditCardPayment());
context.executePayment(100); // Paid 100 via Credit Card

context.setStrategy(new PayPalPayment());
context.executePayment(200); // Paid 200 via PayPal

Advantages:

  • Avoids conditional statements
  • Enhances extensibility
  • Easy to test and maintain

Technology Selection Decisions

Framework Selection Criteria

Evaluation Dimensions:

  1. Community Support: Activity level, documentation quality, issue resolution speed
  2. Learning Curve: Difficulty for team adoption, training costs
  3. Performance: Load speed, runtime performance, memory usage
  4. Ecosystem Richness: Number and quality of available libraries, tools, and plugins
  5. Future Trends: Development direction, community activity, enterprise support

Evaluation Example:

FrameworkCommunity SupportLearning CurvePerformanceEcosystemFuture Trends
React★★★★★★★☆☆☆★★★★☆★★★★★★★★★★
Vue★★★★☆★★★☆☆★★★★☆★★★★☆★★★★☆
Angular★★★☆☆★★★★☆★★★☆☆★★★★☆★★★☆☆
Svelte★★★★☆★★☆☆☆★★★★★★★★☆☆★★★★☆

Decision Recommendations:

  • Team familiar with React → Prioritize React ecosystem
  • Need rapid development → Consider Vue or Svelte
  • Enterprise applications → Angular may be more suitable
  • Performance-focused → Svelte or React

Toolchain Evaluation

Evaluation Dimensions:

  1. Build Speed: Incremental and full build times
  2. Optimization Capabilities: Tree Shaking, Code Splitting, lazy loading
  3. Debugging Support: Source Maps, hot reloading, error tracking
  4. Configuration Complexity: Learning and maintenance costs
  5. Extensibility: Plugin system, customization capabilities

Evaluation Example:

ToolBuild SpeedOptimizationDebugging SupportConfiguration ComplexityExtensibility
Webpack★★★★☆★★★★★★★★★☆★★★★☆★★★★★
Vite★★★★★★★★★☆★★★★☆★★☆☆☆★★★★☆
Rollup★★★★☆★★★★★★★★☆☆★★★☆☆★★★★☆
Parcel★★★★★★★★☆☆★★★☆☆★★☆☆☆★★★☆☆

Decision Recommendations:

  • Prioritize fast development experience → Vite
  • Need fine-grained control → Webpack
  • Library development → Rollup
  • Rapid prototyping → Parcel

Ecosystem Considerations

Evaluation Dimensions:

  1. Libraries and Plugins: Number and quality of available third-party libraries
  2. Tool Integration: Integration with CI/CD, monitoring, and other tools
  3. Community Resources: Tutorials, documentation, case studies
  4. Enterprise Support: Commercial support, professional services
  5. Long-Term Maintenance: Update frequency, roadmap

Evaluation Example:

EcosystemLibraries & PluginsTool IntegrationCommunity ResourcesEnterprise SupportLong-Term Maintenance
React★★★★★★★★★★★★★★★★★★★☆★★★★★
Vue★★★★☆★★★★☆★★★★☆★★★☆☆★★★★☆
Angular★★★☆☆★★★★☆★★★★☆★★★★★★★★★☆
Svelte★★★☆☆★★★☆☆★★★☆☆★★☆☆☆★★★☆☆

Decision Recommendations:

  • Need a rich ecosystem → React or Vue
  • Enterprise applications → Angular
  • Prefer simplicity → Svelte

Extensibility Design

Plugin-Based Architecture

Core Concept: Divide functionality into independent plugins, with the main application extending functionality through plugin interfaces.

Implementation Key Points:

  1. Plugin Interface: Define clear plugin APIs.
  2. Lifecycle Management: Manage plugin loading, initialization, and unloading processes.
  3. Isolation Mechanism: Ensure runtime isolation between plugins.
  4. Communication Mechanism: Define communication methods between the main application and plugins, and among plugins.

Implementation Example:

// Plugin interface definition
interface Plugin {
  name: string;
  init(app: MainApp): void;
  destroy(): void;
}

// Main application plugin management
class MainApp {
  private plugins: Plugin[] = [];
  
  registerPlugin(plugin: Plugin) {
    this.plugins.push(plugin);
    plugin.init(this);
  }
  
  unregisterPlugin(pluginName: string) {
    const plugin = this.plugins.find(p => p.name === pluginName);
    if (plugin) {
      plugin.destroy();
      this.plugins = this.plugins.filter(p => p !== plugin);
    }
  }
}

// Plugin implementation
class AnalyticsPlugin implements Plugin {
  name = 'analytics';
  
  init(app) {
    console.log('Analytics plugin initialized');
    // Integrate analytics functionality
  }
  
  destroy() {
    console.log('Analytics plugin destroyed');
    // Clean up resources
  }
}

// Usage example
const app = new MainApp();
app.registerPlugin(new AnalyticsPlugin());

Advantages:

  • Pluggable functionality
  • Easy to extend
  • Supports third-party development

Middleware Mechanism

Core Concept: Use middleware to intercept and process requests/responses, decoupling cross-cutting concerns (e.g., logging, authentication).

Implementation Key Points:

  1. Middleware Interface: Define a unified middleware function signature.
  2. Execution Chain: Build a middleware execution pipeline.
  3. Context Passing: Share state between middleware.
  4. Error Handling: Capture and propagate errors in middleware.

Implementation Example:

// Middleware type definition
type Middleware = (context: Context, next: () => Promise<void>) => Promise<void>;

// Middleware executor
class MiddlewareExecutor {
  private middlewares: Middleware[] = [];
  
  use(middleware: Middleware) {
    this.middlewares.push(middleware);
  }
  
  async execute(context: Context) {
    const dispatch = async (i: number) => {
      if (i < this.middlewares.length) {
        const middleware = this.middlewares[i];
        await middleware(context, () => dispatch(i + 1));
      }
    };
    await dispatch(0);
  }
}

// Middleware implementation
const loggerMiddleware: Middleware = async (ctx, next) => {
  console.log('Request started');
  await next();
  console.log('Request completed');
};

const authMiddleware: Middleware = async (ctx, next) => {
  if (!ctx.user) {
    throw new Error('Unauthorized');
  }
  await next();
};

// Usage example
const executor = new MiddlewareExecutor();
executor.use(loggerMiddleware);
executor.use(authMiddleware);

executor.execute({ user: { id: 1 } })
  .catch(err => console.error('Error:', err.message));

Advantages:

  • Separation of concerns
  • Composability
  • Easy to test

Extension Point Design

Core Concept: Reserve extension interfaces at critical points in the system, allowing functionality to be added without modifying core code.

Implementation Key Points:

  1. Extension Point Identification: Clearly define the location and purpose of extension points.
  2. Registration Mechanism: Provide mechanisms for registering and discovering extension points.
  3. Execution Order: Control the execution order of extension points.
  4. Context Passing: Pass necessary context information to extension points.

Implementation Example:

// Extension point manager
class ExtensionPointManager {
  private points: Map<string, Function[]> = new Map();
  
  register(pointName: string, extension: Function) {
    if (!this.points.has(pointName)) {
      this.points.set(pointName, []);
    }
    this.points.get(pointName)?.push(extension);
  }
  
  async execute(pointName: string, context: any) {
    const extensions = this.points.get(pointName) || [];
    for (const ext of extensions) {
      await ext(context);
    }
  }
}

// Extension point implementation
const epm = new ExtensionPointManager();

// Register extensions
epm.register('page-loaded', (ctx) => {
  console.log('Page loaded:', ctx.pageName);
});

epm.register('page-loaded', (ctx) => {
  trackPageView(ctx.pageName);
});

// Trigger extension point
epm.execute('page-loaded', { pageName: 'Home' });

Advantages:

  • High flexibility
  • Strong pluggability
  • Supports runtime extensions

Summary

Designing and implementing a micro frontend architecture is a complex system engineering effort that requires consideration of multiple dimensions:

  1. Decomposition Strategies: Divide micro frontend boundaries based on business domains, functional modules, or team structures.
  2. Architectural Principles: Adhere to SOLID principles to ensure robustness and maintainability.
  3. Design Patterns: Apply patterns like Publish-Subscribe, Observer, and Strategy to address specific problems.
  4. Technology Selection: Make informed choices based on framework features, toolchain capabilities, and ecosystem richness.
  5. Extensibility Design: Ensure system extensibility through plugin-based architectures, middleware mechanisms, and extension points.

A successful micro frontend architecture should feature:

  • Clear boundaries and responsibility divisions
  • Loosely coupled inter-module communication
  • Flexible extensibility
  • Strong performance
  • Maintainable technology stack

As frontend technologies continue to evolve, micro frontend architectures will keep advancing, providing more efficient solutions for large-scale application development. Developers should flexibly select and adapt the above design principles and practices based on specific business needs and technical environments.

Share your love