Architectural modularity (how code is internally organized) and service granularity (how functionality is split across network-exposed services) are two distinct design levers that are frequently conflated, and mixing them up leads to costly architectural mistakes.
Overview
- Modularity is an internal concern: it's about codebase organization, team ownership, and ease of change within a single deployable unit.
- Granularity is an external concern: it's about how much functionality each network-exposed service encapsulates, which affects deployment, scaling, and runtime performance.
- Modules and services should not be assumed to map one-to-one - internal cohesion boundaries and external service boundaries are shaped by different forces (technical layering vs. business capability/data ownership).
- Getting granularity wrong is generally costlier to fix than getting modularity wrong, since it involves distributed transactions, data migrations, and cross-team coordination rather than just code refactoring.
- The safest evolutionary path is: modularize first, then extract services from stable, well-understood boundaries - not the reverse.
Key Concepts
Architectural Modularity
- Decomposition of a system into discrete modules with clear boundaries, exposing only what's necessary through well-defined interfaces.
- Goal is separation of concerns: parallel team work, easier testing, lower cognitive load, and reusability.
- Success depends on high cohesion, low coupling - not just splitting files into folders. Modules that are physically separated but share mutable state or hidden dependencies produce "spaghetti dependencies," negating modularity's benefits.
- Enables team/module alignment: teams own specific modules, reducing coordination overhead and easing onboarding.
export interface PaymentGateway {
charge(amount: number, currency: string): Promise<boolean>;
refund(transactionId: string, amount: number): Promise<boolean>;
}
// Swappable implementation behind the contract:
export class StripeGateway implements PaymentGateway { /* ... */ }
Service Granularity
- Determines how much functionality a single service encapsulates.
- Fine-grained services (e.g., a standalone auth service) support independent scaling/deployment but add inter-service communication, latency, and monitoring/tracing overhead.
- Coarse-grained services (e.g., one order-management service) simplify transactions but risk becoming monolithic bottlenecks.
- Best aligned to bounded contexts (Domain-Driven Design) so each service reflects a real business capability rather than an arbitrary technical split.
Migration Path: Modularize, Then Extract
- A common misconception is that internal modules should map 1:1 to services - they shouldn't, since module boundaries reflect internal cohesion while service boundaries reflect business capability and data ownership.
- Recommended sequence: modularize the monolith -> let bounded contexts stabilize -> extract services incrementally using metrics like deployment friction, team autonomy, and operational complexity.
- This evolutionary approach is called a "modular monolith" - start coarse, refactor into finer-grained services as the org matures, avoiding both premature microservices and monolithic rigidity.
Supporting Patterns
- Dependency Injection / IoC: decouples implementation from usage, letting components be swapped for testing, scaling, or migration.
- API Gateway: aggregates endpoints and centralizes cross-cutting concerns (auth, rate limiting) for fine-grained services.
- Service Mesh (Istio, Linkerd): adds observability, security, and reliability to distributed fine-grained architectures.
- Backend-for-Frontend (BFF): dedicated service per client type (mobile vs. web) to avoid overloading one generic gateway.
- Database-per-Service: prevents accidental coupling but introduces distributed-transaction/eventual-consistency challenges, typically solved with event-driven messaging (Kafka, RabbitMQ).
Trade-offs / Caveats
- Fine-grained services: agility and independent scaling, at the cost of latency, orchestration complexity, and partial-failure risk.
- Coarse-grained services: simpler transactions, at the cost of scaling bottlenecks and slower, riskier deployments as scope grows.
- Over-modularization produces a "big ball of mud" - superficially separated modules that are secretly tightly coupled via shared state or leaky abstractions.
- God Module / God Service: consolidating too many responsibilities into one place to avoid proliferation - becomes a deployment bottleneck and regression risk.
- Nano-services: splitting too finely causes cascading failures and excessive orchestration overhead.
- Macro-services: overly broad services that quietly become distributed monoliths, eroding independent scaling and clear ownership.
- No initial boundary cut - module or service - is ever final; both require periodic reassessment via architectural reviews, ADRs, and retrospectives as business needs shift.
Example in Practice
An e-commerce platform illustrates the full spectrum: internally, payment, inventory, and auth are cleanly separated TypeScript modules with dependency-injected contracts (OrderManager depends on a PaymentProcessor interface, not a concrete class). Externally, during Black Friday, only the inventory and order-processing services need to scale - if the team over-fragments into "nano-services" (separate services for tax calc, currency conversion, discount codes, etc.), a single order now requires many chained network calls and any one outage cascades into checkout failures. The balanced approach: keep tightly related, always-co-invoked logic (e.g., pricing) consolidated into one service aligned to a bounded context, and use event-driven messaging (publish_event('OrderPlaced', ...)) to coordinate the independently-owned inventory and shipping databases without reintroducing tight coupling.
Related topics
- Domain-Driven Design (bounded contexts, event storming, value stream mapping)
- Distributed systems observability tooling (Prometheus, Grafana, OpenTelemetry)
- Monorepo tooling for dependency/version management (Yarn Workspaces, Nx)