How to Organize Modules in Software Architecture

    21 Feb, 2023

    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

    System TypeDependency PatternNotable Implementations
    Frontend FrameworksDAGReact Component Tree, Vue SFC
    Build SystemsDAGWebpack, Turborepo, Bazel
    MicroservicesTree/DAG HybridAWS Lambda Layers, NestJS
    Database SchemasDAGForeign Key Relationships

    Best Practices Checklist

    1. Static Analysis

      • Enable TypeScript's --noImplicitCircularity
      • Use ESLint import/no-cycle rule
    2. Dependency Visualization

      npx madge --image graph.svg src/index.ts
      
    3. Architectural Guardrails

      • Enforce layer access rules in tsconfig.json
      • Implement module boundary checks in CI pipelines
    4. 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.