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:
- Business Domain Analysis: Identify the main business domains in the system (e.g., user management, order management, payment management).
- Domain Boundary Definition: Clearly define the responsibilities and interaction points of each business domain.
- 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:
- Function Decomposition: Break down system functions into independent functional modules.
- Module Evaluation: Assess the independence and reusability of each functional module.
- 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:
- Team Division: Clearly define the development teams and their responsibilities within the organization.
- Capability Matching: Match system functions with team capabilities.
- 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:
- Micro Frontend Applications: Each micro frontend application should focus on a specific business function or domain.
- Component Design: Components should focus on a single function, avoiding becoming “God components.”
- 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:
- Plugin Architecture: Design a pluggable micro frontend architecture to support functional extensions through plugins.
- Configuration-Driven: Achieve functional changes through configuration rather than code modification.
- 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:
- Dependency Injection: Manage dependencies between micro frontends through dependency injection.
- Interface Abstraction: Define clear interface specifications to achieve decoupling.
- 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:
- Micro Frontend Communication: Facilitates communication between main and sub-applications.
- Event Bus: Builds a global event system.
- 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:
- Dynamic Algorithm Switching: Select different implementations based on conditions.
- Behavior Parameterization: Pass behavior as a parameter.
- 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:
- Community Support: Activity level, documentation quality, issue resolution speed
- Learning Curve: Difficulty for team adoption, training costs
- Performance: Load speed, runtime performance, memory usage
- Ecosystem Richness: Number and quality of available libraries, tools, and plugins
- Future Trends: Development direction, community activity, enterprise support
Evaluation Example:
| Framework | Community Support | Learning Curve | Performance | Ecosystem | Future 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:
- Build Speed: Incremental and full build times
- Optimization Capabilities: Tree Shaking, Code Splitting, lazy loading
- Debugging Support: Source Maps, hot reloading, error tracking
- Configuration Complexity: Learning and maintenance costs
- Extensibility: Plugin system, customization capabilities
Evaluation Example:
| Tool | Build Speed | Optimization | Debugging Support | Configuration Complexity | Extensibility |
|---|---|---|---|---|---|
| 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:
- Libraries and Plugins: Number and quality of available third-party libraries
- Tool Integration: Integration with CI/CD, monitoring, and other tools
- Community Resources: Tutorials, documentation, case studies
- Enterprise Support: Commercial support, professional services
- Long-Term Maintenance: Update frequency, roadmap
Evaluation Example:
| Ecosystem | Libraries & Plugins | Tool Integration | Community Resources | Enterprise Support | Long-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:
- Plugin Interface: Define clear plugin APIs.
- Lifecycle Management: Manage plugin loading, initialization, and unloading processes.
- Isolation Mechanism: Ensure runtime isolation between plugins.
- 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:
- Middleware Interface: Define a unified middleware function signature.
- Execution Chain: Build a middleware execution pipeline.
- Context Passing: Share state between middleware.
- 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:
- Extension Point Identification: Clearly define the location and purpose of extension points.
- Registration Mechanism: Provide mechanisms for registering and discovering extension points.
- Execution Order: Control the execution order of extension points.
- 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:
- Decomposition Strategies: Divide micro frontend boundaries based on business domains, functional modules, or team structures.
- Architectural Principles: Adhere to SOLID principles to ensure robustness and maintainability.
- Design Patterns: Apply patterns like Publish-Subscribe, Observer, and Strategy to address specific problems.
- Technology Selection: Make informed choices based on framework features, toolchain capabilities, and ecosystem richness.
- 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.



