Modular Architectures: 10 Principles for Building Evolutionary Systems
Waldemar NetoΒΉ, William CalderipeΒΉ
ΒΉTechLeads.club
Abstract
The software industry faces a fundamental granularity dilemma: traditional monoliths suffer from high local complexity and limited scalability, while microservices introduce premature operational complexity that often outweighs their benefits. Recent empirical evidence, including research conducted by Google teams (Ghemawat et al., 2023), demonstrates that microservices generate performance loss, reduced visibility, management difficulties, API freezing, and development slowdown. This work presents Modular Architectures as a formal framework positioned between these extremes, based on conscious separation between logical boundaries (modules) and physical boundaries (deployment). We propose 10 fundamental principles that form a cohesive framework for building systems that evolve sustainably: Well-Defined Boundaries, Composability, Independence, State Isolation, Explicit Communication, Replaceability, Deployment Independence, Individual Scale, Observability, and Fail Independence. Our main contribution is the formalization of these principles as an architectural model that enables gradual evolution of granularity, reducing cognitive load and maximizing delivery velocity without incurring premature complexity.
Keywords: software architecture, modularity, monorepos, microservices, Domain-Driven Design, software granularity
Table of Contents
- Introduction and Contextualization
- State of the Art and Limitations
- Modular Architectures - Formal Definition
- The 10 Fundamental Principles
- Implementation Patterns
- Organizational Governance
- Comparative Analysis and Trade-offs
- Related Work
- Conclusion
- References
1. Introduction and Contextualization
The evolution of software architectures in recent decades reflects profound changes in both technological infrastructure and organizational practices. From the traditional monoliths of the 1990s-2000s, through Service-Oriented Architecture (SOA) in the 2000s, to the microservices that dominated the 2010s, each paradigm brought promises of better organization, scalability, and maintainability.
1.1 The Granularity Problem in Software
Granularity defines the size and cohesion of units that compose a system β whether functions, classes, modules, or services. This is one of the most critical architectural decisions, as it directly impacts:
- Technical complexity: amount of abstractions and integration points
- Cognitive load: volume of information developers need to manage
- Evolution velocity: ease of implementing changes
We can classify granularity into three levels:
- Coarse-grained: larger units with multiple grouped responsibilities (e.g., traditional monoliths)
- Medium-grained: cohesive modules representing business domains (e.g., modular architectures)
- Fine-grained: small, specific units with independent deployment (e.g., microservices)
1.2 Paradigm Shift: Cloud and Modern Tooling
The advent of cloud computing, containers, orchestration (Kubernetes), and serverless fundamentally changed architectural possibilities. For the first time, it became viable to:
- Modularize systems and scale each part independently
- Deploy selectively without recompiling the entire system
- Manage complex dependencies with sophisticated tools (Nx, Bazel, Turborepo)
Paradoxically, this flexibility created a new challenge: complexity migrated from code to distributed architecture. As observed by Khononov (2023) in βBalancing Coupling in Software Design,β complexity in modern systems emerges primarily from communication between components, not from the internal code of each one.
1.3 Local vs Global Complexity
In traditional monolithic systems, complexity was local: infrastructure changes (database, frameworks) impacted the entire system. The response was rigid abstraction layers and patterns like Hexagonal Architecture and Clean Architecture.
In modern distributed systems, complexity is global: the challenge is not protecting the domain from the ORM, but ensuring clear contracts between services, managing eventual consistency, observability, and deployment orchestration.
1.4 Objective of This Work
This paper proposes Modular Architectures as an intermediate framework that:
- Maintains the visibility and collaboration of monoliths
- Offers the autonomy and scalability of microservices
- Avoids premature operational complexity
- Enables gradual evolution according to organizational maturity
We formalize this concept through 10 fundamental principles empirically validated in enterprise projects at companies like Atlassian, BuildOps, and various high-growth startups.
2. State of the Art and Limitations
2.1 Traditional Monoliths
Characteristics:
- All code in a single codebase
- Single, atomic deployment
- Internal communication via function calls
- Shared state (monolithic database)
Benefits:
- Operational simplicity
- Native ACID transactions
- Facilitated debugging
- Lower network overhead
Limitations:
- Limited scalability (scale everything or nothing)
- High accidental coupling
- Risky changes (large blast radius)
- Difficulty in team parallelization
2.2 Microservices: Promises vs Reality
Microservices architecture was widely adopted with promises of:
- Individual scalability per service
- Failure isolation
- Independent release cycles
- Team autonomy
- Clear abstraction boundaries
2.3 Empirical Evidence of Problems
The article βTowards Modern Development of Cloud Applicationsβ (Ghemawat et al., 2023), published at HotOS and developed by Google researchers, including Sanjay Ghemawat (co-creator of MapReduce and Google File System), identifies five systemic problems in microservices:
2.3.1 Performance Loss
Data serialization and network calls add significant overhead compared to local calls. The more fragmented the system, the higher the communication cost.
2.3.2 Loss of Visibility
With multiple versions of multiple services in production simultaneously, it becomes extremely difficult to predict emergent behaviors. Most critical failures occur in interactions between different versions of services.
2.3.3 Management Difficulty
Each service becomes an independent binary with its own build, test, and deployment process. The effort to keep everything working together grows exponentially.
2.3.4 API Freezing
When publishing an API between services, you freeze its evolution. Old APIs persist indefinitely because thereβs no visibility of who consumes them, creating cumulative technical debt.
2.3.5 Development Slowdown
Changes affecting multiple services cannot be made atomically. Developers need to orchestrate changes across N services with misaligned versions, drastically reducing delivery velocity.
2.4 The Fundamental Error
According to Ghemawat et al. (2023), the error of microservices is in conflating logical boundaries (code) with physical boundaries (deployment):
- Microservices force deployment decisions at code-writing time
- This locks the system: contracts and integration points become immutable
- Undoing separation or reorganizing becomes nearly impossible
2.5 Comparative Table: Granularity
| Criteria | Coarse (Monolith) | Medium (Modular) | Fine (Microservices) |
|---|---|---|---|
| Operational Complexity | Low | Medium | High |
| Initial Velocity | High | High | Low |
| Scalability | Limited | Flexible | Maximum |
| System Visibility | Total | High | Fragmented |
| Deployment | Single | Per app | Per service |
| Coupling | High | Controlled | Distributed |
| Required Governance | Low | Medium | High |
graph LR
A[Monolith<br/>Complexity<br/>Medium] --> B[Modular<br/>Complexity<br/>Minimal]
B --> C[Microservices<br/>Complexity<br/>High]
Figure 1: Operational Complexity Sweet Spot
Modular Architectures occupy the point of minimal operational complexity, balancing the benefits of monoliths (simplicity) with those of microservices (autonomy), avoiding the problems of both extremes.
2.6 Identified Gap
There is no formal framework that allows:
- Starting simple and evolving gradually
- Separating modularization decisions from deployment decisions
- Maintaining total visibility while offering autonomy
- Avoiding irreversible granularity commitments
Modular Architectures fill this gap.
3. Modular Architectures - Formal Definition
3.1 Definition
Modular Architecture is a software system that establishes explicit separation between logical boundaries (modules) and physical boundaries (processes/deployments), allowing granularity to evolve incrementally and reversibly according to business needs and organizational maturity.
3.2 Fundamental Characteristics
3.2.1 Modularization in Monorepo
All code lives in a single versioned repository, organized into:
- Apps: bootstrap points that orchestrate modules
- Packages: logical units containing business domains or infrastructure
3.2.2 Independent Deployment as an Option
Modules can be deployed together (modular monolith) or separately (dedicated apps), without requiring code refactoring. The deployment decision is operational, not structural.
3.2.3 Gradual Evolution
The architecture supports natural transitions:
Modular Monolith β Modules with Clear Boundaries β
Dedicated Apps β Microservices (when justified)
3.3 Architectural Components
3.3.1 Apps (Bootstrap Points)
Apps are executables that import and initialize modules. They contain no business logic.
apps/
monolith/ # Imports all modules
main.ts # Bootstrap
monolith.module.ts # Orchestration
billing-api/ # Imports only Billing
main.ts
billing-api.module.ts
3.3.2 Packages (Logical Units)
Domain Modules: encapsulate business rules of a Bounded Context (DDD)
packages/
billing/
core/ # Entities, use cases
persistence/ # Repositories, migrations
http/ # Controllers, DTOs
__test__/ # Isolated tests
Infrastructure Modules: provide reusable technical capabilities
packages/
shared/
logger/
config/
database/
3.3.3 Explicit Dependency Graph
Tools like Nx automatically build a graph showing how modules relate, enabling:
- Detection of circular dependencies
- Running tests only on affected modules (
nx affected) - Incremental builds
monorepo-root/
β
βββ apps/ [Apps = Bootstrap]
β βββ monolith/ β Imports all modules
β β βββ main.ts
β β βββ monolith.module.ts
β β
β βββ billing-api/ β Only Billing
β β βββ main.ts
β β βββ billing-api.module.ts
β β
β βββ content-worker/ β Only Content/VideoProcessor
β βββ main.ts
β βββ content-worker.module.ts
β
βββ packages/ [Packages = Logic]
β β
β βββ DOMAINS
β β β
β β βββ billing/ β Bounded Context
β β β βββ core/ (entities, use cases)
β β β βββ persistence/ (repositories, migrations)
β β β βββ http/ (controllers, DTOs)
β β β βββ __test__/
β β β
β β βββ content/
β β β βββ catalog/ β Submodule
β β β βββ video-processor/ β Submodule
β β β βββ admin/ β Submodule
β β β
β β βββ identity/
β β βββ core/
β β βββ persistence/
β β βββ http/
β β
β βββ INFRASTRUCTURE
β βββ shared/
β βββ logger/ (no business logic)
β βββ config/
β βββ database/
β
βββ tools/
β βββ scripts/
β
βββ nx.json [Nx Configuration]
βββ package.json
βββ tsconfig.base.json
3.4 Enabling Tooling
Modular Architectures are enabled by modern tools:
| Technology | Capability | Examples |
|---|---|---|
| Monorepo Tools | Dependency management, incremental builds | Nx, Bazel, Turborepo |
| Modular Frameworks | Composition via modules, dependency injection | NestJS, Spring Boot, .NET |
| Native Modularization | Module systems in languages | Java Modules, Go packages |
| Integrated Solutions | Flexible deployment maintaining modularity | Ktor, Maven Packages, Java Modulith |
3.5 Fundamental Differentiation
| Aspect | Microservices | Modular Architectures |
|---|---|---|
| Deployment Decision | Defined in architecture | Separated from architecture |
| Granularity | Fixed from the start | Evolves gradually |
| Reversibility | Difficult | Natural |
| Initial Complexity | High | Low |
| Visibility | Fragmented | Total (monorepo) |
graph TB
subgraph apps [Apps - Bootstrap]
monolith[monolith/]
billing_api[billing-api/]
content_worker[content-worker/]
end
subgraph packages [Packages - Logic]
subgraph domain [Domains]
billing[billing/]
content[content/]
identity[identity/]
end
subgraph infra [Infrastructure]
shared[shared/]
end
end
monolith --> billing
monolith --> content
monolith --> identity
monolith --> shared
billing_api --> billing
billing_api --> shared
content_worker --> content
content_worker --> shared
style monolith fill:#4A90E2
style billing_api fill:#4A90E2
style content_worker fill:#4A90E2
style billing fill:#50C878
style content fill:#50C878
style identity fill:#50C878
style shared fill:#9CA3AF
4. The 10 Fundamental Principles
This framework is structured into four categories of principles, each addressing a fundamental aspect of modularity.
4.1 Structural Principles
Define the foundation of modular organization.
4.1.1 Well-Defined Boundaries
Definition: Each module has clear responsibilities aligned to a Bounded Context (DDD), without exposing internal details to other modules.
Rationale: Clear boundaries reduce accidental coupling, facilitate maintenance, and enable independent evolution. They follow the encapsulation principle extended to the architectural level.
Implementation:
The following examples show module setup in different languages. The concept applies to any technology - modules can be implemented with or without frameworks, using native language organization and encapsulation conventions.
NestJS (TypeScript) - with framework:
// packages/billing/billing.module.ts
@Module({
imports: [TypeOrmModule.forFeature([SubscriptionEntity])],
providers: [BillingService],
controllers: [BillingController],
exports: [] // Nothing exported - communication via HTTP/events
})
export class BillingModule {}
Go (without framework) - conventions only:
// packages/billing/module.go
package billing
// Module setup - public initialization function
func Setup(router *mux.Router, db *sql.DB) {
// Internal components (not exported)
repo := newRepository(db)
service := newService(repo)
// Only HTTP routes are exposed
setupRoutes(router, service)
}
Ktor (Kotlin) - conventions + extension functions:
// packages/billing/BillingModule.kt
package com.example.billing
fun Application.billingModule() {
// Internal components
val repository = BillingRepository(database)
val service = BillingService(repository)
// Only routes are exposed
routing {
billingRoutes(service)
}
}
Universal principle: The module is an organizational unit that:
- Groups related components (repositories, services, controllers)
- Exposes only public entry points (APIs, events)
- Keeps internal implementations private using language visibility
Common violation: Modules that export repositories or internal entities allow others to depend on implementation details.
4.1.2 Composability
Definition: Modules are designed as reusable blocks that can be combined to form different applications or functionalities.
Rationale: Maximizes reuse, reduces duplication, and enables incremental evolution without rewriting code.
Implementation:
// monolith.module.ts (imports everything)
@Module({
imports: [BillingModule, ContentModule, IdentityModule, LoggerModule]
})
export class MonolithModule {}
// billing-api.module.ts (imports only billing)
@Module({
imports: [BillingModule, LoggerModule]
})
export class BillingApiModule {}
Benefit: Same modules can be orchestrated in different ways without duplicating code.
4.1.3 Independence
Definition: Modules are autonomous in code and infrastructure, communicating only via explicit APIs, events, or asynchronous messages.
Rationale: Prevents accidental coupling, enables independent technological evolution, and facilitates isolated testing.
Implementation:
- Each module has its own environment variables
- Communication via HTTP, gRPC, or messaging (never direct imports)
- Tests run without needing other modules
Rule: If module A needs data from module B, use API or event β never import classes from B directly.
4.2 Operational Principles
Define how modules interact.
4.2.1 State Isolation
Definition: Each module manages its own state (database, cache, queues) without direct sharing with other modules.
Rationale: Prevents cascading failures, enables independent scalability, and facilitates schema evolution.
Implementation:
graph TB
subgraph "Billing Module"
BillingService[Billing Service]
BillingRepo[Billing Repository]
end
subgraph "Identity Module"
IdentityService[Identity Service]
IdentityRepo[Identity Repository]
end
subgraph "Database"
subgraph "Schema/DB Billing"
BillingTables["Tables:<br/>β’ subscriptions<br/>β’ payments<br/>β’ invoices"]
end
subgraph "Schema/DB Identity"
IdentityTables["Tables:<br/>β’ users<br/>β’ sessions<br/>β’ roles"]
end
end
BillingService --> BillingRepo
BillingRepo --> BillingTables
IdentityService --> IdentityRepo
IdentityRepo --> IdentityTables
BillingRepo -.->|β Never accesses| IdentityTables
IdentityRepo -.->|β Never accesses| BillingTables
style BillingService fill:#4A90E2
style IdentityService fill:#50C878
style BillingTables fill:#FFE5B4
style IdentityTables fill:#E0FFE0
style BillingRepo fill:#4A90E2
style IdentityRepo fill:#50C878
Figure: State Isolation between Modules
Each module accesses exclusively its own tables, without direct state sharing. Communication between modules occurs only via APIs or events.
Database strategies:
- Shared Database with Separate Schemas: isolated schemas or table prefixes (good for starting)
- Completely Separate Databases: total physical isolation (good for maturity)
4.2.2 Explicit Communication
Definition: All communication between modules occurs via well-defined contracts (interfaces, DTOs, event schemas).
Rationale: Avoids implicit coupling, enables contract versioning, and facilitates testing with mocks.
Implementation:
Local (same process):
// Public interface
export interface BillingAPI {
createSubscription(userId: string, planId: string): Promise<Subscription>;
}
// Implementation
@Injectable()
export class BillingService implements BillingAPI {
async createSubscription(...) { /* logic */ }
}
Remote (separate processes):
@Controller('billing')
export class BillingController {
@Post('subscribe')
async createSubscription(@Body() dto: CreateSubscriptionDto) { /* ... */ }
}
Asynchronous (events):
@OnEvent('billing.subscription.created')
handleSubscriptionCreated(payload: SubscriptionCreatedEvent) { /* ... */ }
4.2.3 Replaceability
Definition: Modules can be removed, changed, or replaced without affecting the rest of the architecture.
Rationale: Enables technological experimentation, reduces vendor lock-in, and facilitates refactoring.
Implementation:
- Modules donβt export concrete classes
- External dependencies are abstracted via interfaces
- Public contracts remain stable while implementations change
Example: Switch ORM (TypeORM β Prisma) within a module without impacting consumers.
4.3 Deployment Principles
Define deployment flexibility.
4.3.1 Deployment Independence
Definition: Each app can be versioned and deployed in isolation, without forcing deployments of other apps.
Rationale: Reduces change risk, enables differentiated release cycles, and facilitates rollbacks.
Implementation (Example):
# CI/CD Pipeline (GitHub Actions example)
name: Deploy Billing API
on:
push:
branches: [main]
jobs:
check-affected:
runs-on: ubuntu-latest
steps:
- run: npx nx affected:apps --base=origin/main
- if: contains(steps.affected.outputs.apps, 'billing-api')
run: npx nx build billing-api && npm run deploy:billing-api
Strategy: Use nx affected to detect changes and deploy only impacted apps.
4.3.2 Individual Scale
Definition: Each app can scale horizontally according to its specific demand, without affecting others.
Rationale: Optimizes resource usage and prevents local bottlenecks from impacting the entire system.
Strategy: Metrics per app determine independent scaling.
4.4 Resilience Principles
Define reliable production operation.
4.4.1 Observability
Definition: Each module has its own logs, metrics, and tracing, enabling isolated diagnosis.
Rationale: Identifies problems quickly without searching the entire system.
Implementation:
// Logs with module context
logger.log('Subscription created', {
module: 'billing',
userId,
planId
});
// Metrics per module
@Metrics({ module: 'billing' })
export class BillingService { /* ... */ }
Tooling: Prometheus (metrics), Grafana (dashboards), Jaeger (distributed tracing).
4.4.2 Fail Independence
Definition: Failures in one module donβt propagate, ensuring controlled system degradation.
Rationale: Maximizes availability and facilitates recovery.
Implementation:
// Circuit Breaker
import CircuitBreaker from 'opossum';
@Injectable()
export class BillingClient {
private breaker = new CircuitBreaker(
() => this.callBillingAPI(),
{ timeout: 5000, errorThresholdPercentage: 50 }
);
async getBillingData() {
try {
return await this.breaker.fire();
} catch (err) {
// Fallback
return { status: 'degraded', data: null };
}
}
}
Strategies: Circuit breakers, timeouts, retries with exponential backoff, fallbacks.
The 10 Principles Organized
graph TB
subgraph resilience [Resilience]
P9[9. Observability]
P10[10. Fail Independence]
end
subgraph deploy [Deployment]
P7[7. Deployment Independence]
P8[8. Individual Scale]
end
subgraph operational [Operational]
P4[4. State Isolation]
P5[5. Explicit Communication]
P6[6. Replaceability]
end
subgraph structural [Structural]
P1[1. Well-Defined Boundaries]
P2[2. Composability]
P3[3. Independence]
end
P1 --> P4
P2 --> P5
P3 --> P6
P4 --> P7
P5 --> P8
P6 --> P7
P7 --> P9
P8 --> P10
style resilience fill:#EF4444,color:#fff
style deploy fill:#F97316,color:#fff
style operational fill:#3B82F6,color:#fff
style structural fill:#10B981,color:#fff
5. Implementation Patterns
5.1 Types of Modules
5.1.1 Domain Modules
Represent Bounded Contexts (DDD) and encapsulate specific business rules.
Examples: billing/, content/, identity/
Typical structure:
billing/
βββ core/ # Entities, services, use cases
βββ persistence/ # Repositories, migrations
βββ http/ # Controllers, DTOs
βββ __test__/ # Unit and E2E tests
5.1.2 Infrastructure Modules (Shared)
Provide reusable technical capabilities, without business logic.
Examples: shared/logger/, shared/config/, shared/database/
Rule: Never include business rules in shared. If two domains share logic, it indicates a poorly defined boundary.
5.2 Submodules
When domains become too large, divide them into submodules while maintaining Bounded Context cohesion.
Example:
content/
βββ catalog/ # Submodule: catalog management
βββ video-processor/ # Submodule: video processing
βββ admin/ # Submodule: content administration
Benefits:
- Greater internal cohesion
- Controlled sharing within the domain
- Separate scaling (e.g., video-processor can run as isolated worker)
Rule: Submodules can share entities and repositories, but never services or application logic.
5.3 App Design Patterns
5.3.1 Pattern 1: Shared App + Domain Apps
Structure:
apps/
monolith/ # Shared app (all modules)
billing-api/ # Dedicated app (only Billing)
content-worker/ # Dedicated app (only Content/VideoProcessor)
When to use:
- Small/medium companies
- Domains still being discovered
- Need for initial velocity
Advantages:
- Low operational complexity
- Easy to add new modules
- Enables gradual promotion to dedicated apps
Challenges:
- Shared on-call (requires alerts per module)
- Joint deployments (use feature flags)
5.3.2 Pattern 2: Apps per Domain
Structure:
apps/
billing-app/ # Only Billing modules
content-app/ # Only Content modules
identity-app/ # Only Identity modules
When to use:
- Well-established domains
- Mature teams
- Need for strong isolation
Advantages:
- Maximum autonomy
- Independent pipelines
- Granular scaling
Challenges:
- Greater operational overhead
- Risk of excessive granularity
5.4 Evolution Strategy
Phase 1: Modular Monolith
- One shared app
- All modules together
- Defined logical boundaries
Phase 2: Modules with Clear Boundaries
- Still one shared app
- Strong governance (linters, enforce-module-boundaries)
- Observability per module
Phase 3: Dedicated Apps
- Critical modules promoted to their own apps
- Independent deployment and scaling
- Shared app still exists for smaller modules
Phase 4: Extraction to Microservices
- Only when necessary (compliance, extreme scale)
- Communication exclusively via network
- Justified operational cost
timeline
title Evolution of Modular Architectures
section Phase 1
Modular Monolith : 1 app
: All modules
: Maximum velocity
section Phase 2
Clear Boundaries : Enforce boundaries
: Observability per module
: Feature flags
section Phase 3
Dedicated Apps : 2+ apps
: Independent deployment
: Selective scaling
section Phase 4
Microservices : Many teams
: Network mandatory
: Maximum autonomy
6. Organizational Governance
6.1 Inverse Conway Architecture
Conwayβs Law establishes that systems reflect organizational structures. In Modular Architectures, we invert this: code structure influences organization.
Well-defined modules naturally create:
- Clear ownership
- Team autonomy
- Reduced organizational dependencies
6.2 Tech Lead Responsibilities
Technical leaders in modular architectures must:
- Establish Separation Guidelines
- When to create a new module
- When to promote a module to a dedicated app
- Cohesion and coupling criteria
- Ensure Observability per Module
- Dedicated dashboards
- Alerts directed to responsible teams
- Incident playbooks
- Balance Autonomy and Coherence
- Autonomy: teams choose implementations
- Coherence: contracts and patterns are consistent
- Manage Architectural Evolution
- Review dependency graph periodically
- Identify accidental coupling
- Facilitate boundary refactorings
6.3 Versioning: Fast Forward vs Time Machine
6.3.1 Fast Forward (Monorepo)
- Everyone always on the latest version
- Zero βbump hellβ
- Pipeline ensures consistency via
nx affected - Ideal for velocity and consistency
6.3.2 Time Machine (Polyrepo)
- Each package with its own version
- Possibility of staying on old versions
- High synchronization cost
- Generates maintenance complexity
Recommendation: Fast Forward for evolutionary architectures.
6.4 Operational Challenges
6.4.1 Shared On-Call
Problem: Who handles alerts when multiple teams share an app?
Solution:
- Alerts with module tag
- Automatic routing based on ownership
- Separate dashboards per domain
6.4.2 Dependency Management
Problem: Modules can create circular or unwanted dependencies.
Solution:
// nx.json - enforce-module-boundaries
{
"tags": {
"domain:billing": ["domain:billing", "shared"],
"domain:content": ["domain:content", "shared"],
"shared": []
}
}
Rule: Domains only depend on shared, never on each other.
6.4.3 Feature Flags for Joint Deployments
Problem: Shared app deployments affect multiple teams.
Solution:
if (featureFlags.isEnabled('billing-new-checkout', userId)) {
return newCheckoutFlow();
}
return legacyCheckoutFlow();
7. Comparative Analysis and Trade-offs
7.1 Detailed Comparative Table
| Criteria | Monolith | Modular | Microservices |
|---|---|---|---|
| Operational Complexity | Low | Medium | High |
| Initial Velocity | High | High | Low |
| Scalability | Vertical | Selective horizontal | Total horizontal |
| Visibility | Total | Total (monorepo) | Fragmented |
| Deployment | Single | Per app | Per service |
| Coupling | High | Controlled | Distributed |
| Cognitive Load | High (local) | Medium | High (global) |
| Operational Cost | Low | Medium | High |
| Team Autonomy | Low | Medium-High | High |
| Reversibility | Difficult | Easy | Very difficult |
7.2 When to Use Modular Architectures
Yes, when:
- Medium to large teams (5-100+ people)
- Domains in evolution or discovery
- Need for frequent collaboration between teams
- Mixed scaling requirements (some modules need to scale, others donβt)
- Established monorepo culture
- Priority on delivery velocity
7.3 When NOT to Use
Consider alternatives when:
Prefer Simple Monolith:
- Very small system (1-3 developers)
- Extremely simple domain
- Rapid prototyping
Prefer Microservices:
- Totally stable and isolated domains
- Need for total physical isolation (compliance, regulatory)
- Completely independent teams geographically
- Extreme heterogeneous scaling requirements
7.4 Identified Limitations
- Requires Governance Discipline
- Without enforcement, boundaries degrade
- Requires active technical leaders
- Specific Tooling
- Nx, Bazel, or equivalent are necessary
- Initial learning curve
- Shared Database
- If using single database, depends on discipline for isolation
- No physical guarantees of separation
- Monorepo Culture
- Teams used to polyrepo may resist
- Requires cultural change
8. Related Work
8.1 Domain-Driven Design (Evans, 2003)
Contribution: Concept of Bounded Contexts as modeling units.
Relation: Modular Architectures implement Bounded Contexts as domain modules, with technical boundaries beyond conceptual ones.
Reference: Evans, E. (2003). Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley.
8.2 Towards Modern Development of Cloud Applications (Ghemawat et al., 2023)
Contribution: Empirical evidence of systemic problems in microservices.
Relation: Validates the need to separate logical from physical boundaries, conceptual basis of this work.
Reference: Ghemawat, S., et al. (2023). Towards Modern Development of Cloud Applications. HotOS β23.
8.3 Java Modulith (Spring Ecosystem)
Contribution: Modularity in the Java Spring Boot ecosystem.
Relation: Implements similar principles of Bounded Contexts in Java monorepos.
Difference: Specific to Java; this work generalizes to multiple languages.
8.4 Balancing Coupling in Software Design (Khononov, 2023)
Contribution: Analysis of global vs local complexity in distributed systems.
Relation: Substantiates the argument that modern complexity is in communication, not internal code.
Reference: Khononov, V. (2023). Balancing Coupling in Software Design. Addison-Wesley.
8.5 Nx Documentation (Nrwl)
Contribution: Monorepo practices and patterns focused on modularity.
Relation: Tooling that enables practical implementation of principles.
Reference: Nrwl. (2024). Nx Documentation. https://nx.dev
8.6 NestJS (Kamil Mysliwiec)
Contribution: Node.js framework with modular architecture inspired by Angular, promoting clear separation of responsibilities via modules, dependency injection, and decorators.
Relation: Practical example of framework that facilitates implementation of modular architectures, demonstrating how modular structures can be implemented natively in modern ecosystems.
Reference: Mysliwiec, K. et al. (2024). NestJS - A progressive Node.js framework. https://nestjs.com
8.7 Strategic Monoliths and Microservices (Vernon, 2021)
Contribution: Pragmatic analysis of when and how to use monoliths versus microservices, emphasizing purposeful architecture and the importance of architectural decisions aligned with business context.
Relation: Complements this work by recognizing that not every system needs microservices and that well-structured (modular) monoliths can be the most strategic choice. Vernon highlights the importance of Bounded Contexts and internal modularity, central concepts in our framework.
Reference: Vernon, V. (2021). Strategic Monoliths and Microservices: Driving Innovation Using Purposeful Architecture. Addison-Wesley Signature Series.
8.8 Enterprise Monorepo Angular Patterns (Nrwl/Nx Team)
Contribution: Practical patterns for organizing monorepos in enterprise environments, focusing on team scalability, code sharing, and dependency governance.
Relation: Validates in practice the monorepo modularization concepts presented in this work. Demonstrates how tools like Nx enable modular architectures in large-scale enterprise projects, with concrete examples of project structuring, libraries, and apps.
Reference: Nrwl. Enterprise Monorepo Angular Patterns. Nrwl Press.
8.9 Fundamentals of Software Architecture (Richards & Ford, 2020)
Contribution: Academic formalization of different architectural styles, including modular monoliths as a valid architectural pattern. Presents framework for analyzing trade-offs and architectural characteristics.
Relation: Provides theoretical basis for classifying modular architectures as a distinct architectural style. Validates the importance of characteristics like modularity, deployability, and testability that underpin our 10 principles.
Reference: Richards, M., & Ford, N. (2020). Fundamentals of Software Architecture: An Engineering Approach. OβReilly Media.
8.10 Software Architecture: The Hard Parts (Ford, Richards et al., 2021)
Contribution: Deep analysis of trade-offs in modern distributed architectures, questioning when microservices really pay off and exploring alternatives like modular monoliths.
Relation: Complements this work by providing methodology for analyzing architectural trade-offs. Reinforces the message that architectural decisions should be based on context and real needs, not hype.
Reference: Ford, N., Richards, M., Sadalage, P., & Dehghani, Z. (2021). Software Architecture: The Hard Parts: Modern Trade-Off Analyses for Distributed Architectures. OβReilly Media.
8.11 Building Evolutionary Architectures (Ford, Parsons, Kua, 2017)
Contribution: Concept of fitness functions and architectures that evolve incrementally over time, supporting constant change through engineering practices.
Relation: Directly substantiates the gradual evolution proposal of this framework. The concept of fitness functions can be applied to validate that modules maintain their boundaries (via enforce-module-boundaries) and that principles are respected over time.
Reference: Ford, N., Parsons, R., & Kua, P. (2017). Building Evolutionary Architectures: Support Constant Change. OβReilly Media.
8.12 Team Topologies (Skelton & Pais, 2019)
Contribution: Framework for structuring technology teams based on workflow, defining patterns like stream-aligned teams, enabling teams, and platform teams.
Relation: Essential for the Organizational Governance section of this work. Modular architectures naturally align with stream-aligned teams, where each team owns one or more domain modules, with platform teams providing infrastructure modules (shared).
Reference: Skelton, M., & Pais, M. (2019). Team Topologies: Organizing Business and Technology Teams for Fast Flow. IT Revolution Press.
8.13 Monolith to Microservices (Newman, 2019)
Contribution: Pragmatic guide on monolith decomposition, recognizing that not all systems should become microservices and presenting patterns for incremental transition when necessary.
Relation: Validates the gradual evolution approach proposed in this framework. Newman demonstrates that transition should be conscious and incremental, exactly what modular architectures facilitate by establishing clear boundaries from the start.
Reference: Newman, S. (2019). Monolith to Microservices: Evolutionary Patterns to Transform Your Monolith. OβReilly Media.
8.14 Differentiation of This Work
This paper goes beyond by:
- Formalizing 10 principles as a cohesive framework
- Proposing implementation patterns empirically validated
- Including organizational governance as an integral part
- Establishing gradual evolution strategy between paradigms
9. Conclusion
9.1 Synthesis
Modular Architectures represent a βconscious middle groundβ between traditional monoliths and microservices, grounded in:
- Explicit separation between logical and physical boundaries
- Gradual evolution of granularity according to maturity
- Total visibility via monorepo while maintaining autonomy
- Controlled complexity avoiding premature overhead
9.2 Main Contributions
This work offers:
- Formal Framework with 10 Principles
- Structural: Boundaries, Composability, Independence
- Operational: State Isolation, Communication, Replaceability
- Deployment: Deployment Independence, Individual Scale
- Resilience: Observability, Fail Independence
- Validated Implementation Patterns
- Module types (domain vs infrastructure)
- Submodules for large domains
- App design patterns (shared vs dedicated)
- Organizational Governance Model
- Tech lead responsibilities
- Fast Forward versioning
- Management of operational challenges
- Gradual Evolution Strategy
- 4 phases: Modular Monolith β Clear Boundaries β Dedicated Apps β Microservices
- Reversible decisions at each phase
9.3 Positioning
Modular Architectures are not revolution, but natural evolution of:
- Domain-Driven Design (concepts)
- Modern tooling (technical enablement)
- Empirical evidence (scientific validation)
9.4 Future Work
Research directions:
- Quantitative Metrics of Cognitive Load
- Formalize metrics to measure perceived complexity
- Compare with monoliths and microservices
- Longitudinal Case Studies
- Track evolution of real systems over years
- Validate technical debt reduction hypothesis
- Validation in Different Ecosystems
- Java, Go, .NET, Python
- Identify universal vs language-specific patterns
- Automated Governance Tooling
- Static analysis of principle violations
- Automated refactoring suggestions
9.5 Openness
This work is an open contribution to the community. We encourage:
- Feedback and constructive criticism
- Implementations in different contexts
- Empirical validation in new domains
- Extensions and refinements of the framework
References
-
Evans, E. (2003). Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley Professional.
-
Ghemawat, S., Grandl, R., Petrovic, S., Schwarzkopf, M., Whittaker, M., & McKinley, K. S. (2023). Towards Modern Development of Cloud Applications. In Proceedings of the 19th Workshop on Hot Topics in Operating Systems (HotOS β23). ACM. https://doi.org/10.1145/3593856.3595909
-
Khononov, V. (2023). Balancing Coupling in Software Design: Successful Software Architecture in General and Distributed Systems in Particular. Addison-Wesley Professional.
-
Newman, S. (2021). Building Microservices: Designing Fine-Grained Systems (2nd ed.). OβReilly Media.
-
Nrwl. (2024). Nx: Smart Monorepos Β· Fast CI. Retrieved from https://nx.dev
-
Mysliwiec, K. et al. (2024). NestJS: A progressive Node.js framework. Retrieved from https://nestjs.com
-
Google. (2023). Service Weaver: A Framework for Writing Distributed Applications. Retrieved from https://serviceweaver.dev
-
Spring. (2024). Modulith: A Modular Monolith Toolkit for Spring Boot. Retrieved from https://spring.io/projects/spring-modulith
-
Fowler, M. (2014). Microservices: A Definition of This New Architectural Term. Retrieved from https://martinfowler.com/articles/microservices.html
-
Vernon, V. (2013). Implementing Domain-Driven Design. Addison-Wesley Professional.
-
Vernon, V. (2021). Strategic Monoliths and Microservices: Driving Innovation Using Purposeful Architecture. Addison-Wesley Signature Series.
-
Nrwl. Enterprise Monorepo Angular Patterns. Nrwl Press.
-
Richards, M., & Ford, N. (2020). Fundamentals of Software Architecture: An Engineering Approach. OβReilly Media.
-
Ford, N., Richards, M., Sadalage, P., & Dehghani, Z. (2021). Software Architecture: The Hard Parts: Modern Trade-Off Analyses for Distributed Architectures. OβReilly Media.
-
Ford, N., Parsons, R., & Kua, P. (2017). Building Evolutionary Architectures: Support Constant Change. OβReilly Media.
-
Skelton, M., & Pais, M. (2019). Team Topologies: Organizing Business and Technology Teams for Fast Flow. IT Revolution Press.
-
Newman, S. (2019). Monolith to Microservices: Evolutionary Patterns to Transform Your Monolith. OβReilly Media.
-
Richardson, C. (2018). Microservices Patterns: With Examples in Java. Manning Publications.
About the Authors
Waldemar Neto is co-founder of TechLeads.club and has experience in software architecture at companies like ThoughtWorks, Atlassian, and high-growth startups. Specialist in evolutionary systems and software engineering practices.
- LinkedIn: https://www.linkedin.com/in/waldemarnt/
William Calderipe is co-founder of TechLeads.club with experience in enterprise development at Atlassian and other large-scale organizations. Focus on scalable architectures and DevOps practices.
- LinkedIn: https://www.linkedin.com/in/wcalderipe/
Contact:
- Website: https://techleads.club
- Email: waldemarnt@gmail.com
- LinkedIn Waldemar Neto: https://www.linkedin.com/in/waldemarnt/
- LinkedIn William Calderipe: https://www.linkedin.com/in/wcalderipe/
License: This work is licensed under Creative Commons Attribution 4.0 International (CC BY 4.0).
You are free to:
- β Share β copy and redistribute the material in any medium or format
- β Adapt β remix, transform, and build upon the material for any purpose, even commercially
Under the following terms:
- π Attribution β You must give appropriate credit, provide a link to the license, and indicate if changes were made
More information: https://creativecommons.org/licenses/by/4.0/
How to cite this work:
Neto, W., & Calderipe, W. (2025). Modular Architectures: 10 Principles for
Building Evolutionary Systems. TechLeads.club Whitepaper, v1.0.
Version: 1.0
Publication Date: October 13, 2025
Status: Initial Publication β Feedback Welcome