How to Organize Modules in Software Architecture
In software engineering, the reference relationship of software modules should ideally be a tree, but in practical applications, in order to reuse certain module structures, a directed acyclic graph is often formed.
In software engineering, while module dependencies ideally form a tree structure, real-world implementations often adopt Directed Acyclic Graphs (DAGs) to enable practical code reuse. This article explores three fundamental patterns of module organization and their implications.
Ideal Scenario: Hierarchical Tree Structure
Characteristics of Tree Architecture
Key Features:
- Unidirectional dependencies (parent → child)
- Clear hierarchical separation (e.g., UI → Business Logic → Data Layer)
- No circular references
Common Implementations:
- MVC Architecture
- Clean Architecture
- Three-Layer Pattern (Presentation/Logic/Data)
Advantages:
✅ Straightforward dependency management
✅ Explicit separation of concerns
✅ Simplified testing and maintenance
Challenges:
⚠️ Limited code reuse opportunities
⚠️ Potential for artificial module fragmentation
⚠️ Suboptimal for complex systems with cross-cutting concerns
Practical Reality: Directed Acyclic Graph (DAG)
Modern Dependency Management
Key Features:
- Shared modules between components (E shared by B/C)
- Multiple dependency paths
- Strictly acyclic
Real-World Applications:
- React/Vue Component Systems
- Webpack Module Federation
- Bazel Build System
- Database Schema Design
Advantages:
✅ Natural code reuse through shared modules
✅ Flexible architecture for complex systems
✅ Maintains dependency integrity
Implementation Considerations:
🔧 Requires robust tooling (TypeScript path resolution, Webpack aliases)
🔧 Needs dependency visualization tools
🔧 Demands strict code review practices
Anti-Pattern: Circular Dependencies
Dangerous Dependency Loops
Consequences:
❌ Runtime initialization errors (Cannot access 'X' before initialization
)
❌ Compilation failures (ESM/TypeScript circular reference errors)
❌ Unmaintainable "spaghetti code" structure
Common Culprits:
- Bidirectional controller-service relationships
- Shared state management traps
- Improper dependency injection
Architectural Strategies
1. Dependency Hoisting
Elevate shared dependencies to higher-level modules to prevent duplication while maintaining acyclicity.
2. Dependency Injection
Implement IoC containers to manage cross-module dependencies:
// Angular-style DI example
@Injectable({ providedIn: 'root' })
export class SharedService {
// Reusable business logic
}
3. Module Federation
Modern solutions for microfrontend architectures:
// Webpack Module Federation Configuration
new ModuleFederationPlugin({
name: 'app1',
exposes: {
'./Widget': './src/components/Widget',
}
})
Industry Case Studies
Best Practices Checklist
-
Static Analysis
- Enable TypeScript's
--noImplicitCircularity
- Use ESLint
import/no-cycle
rule
- Enable TypeScript's
-
Dependency Visualization
npx madge --image graph.svg src/index.ts
-
Architectural Guardrails
- Enforce layer access rules in
tsconfig.json
- Implement module boundary checks in CI pipelines
- Enforce layer access rules in
-
Refactoring Techniques
- Apply Dependency Inversion Principle (DIP)
- Utilize facade patterns for cross-module communication
"Good architecture maximizes the number of decisions not made."
― Robert C. Martin (Clean Architecture)
By understanding these fundamental patterns and implementing appropriate guardrails, teams can create maintainable systems that balance architectural purity with practical reuse requirements.