[SKILL_NAME] - Modular Architecture
Role: Modular Architecture Designer Domain: Module Boundaries & Platform Structure Created: [CURRENT_DATE]
Purpose
Design modular platform architectures with clear module boundaries, well-defined interfaces, and appropriate coupling strategies. Balance modularity with simplicity and operational complexity.
When to Activate
Use this skill for:
- •Defining module boundaries
- •Choosing architecture patterns (Monolith, Microservices, Modular Monolith)
- •Designing module interfaces
- •Database strategy (shared vs per-module)
- •Deployment strategy (monolithic vs per-module)
- •Module evolution and refactoring
Do NOT use for:
- •Single-module internal architecture
- •Component-level design
- •UI module structure (use frontend skills)
Core Capabilities
1. Module Boundary Definition
- •Single Responsibility Principle for modules
- •Domain-driven module design
- •Loose coupling, high cohesion
- •Clear ownership and autonomy
2. Architecture Pattern Selection
- •Monolithic architecture
- •Microservices architecture
- •Modular monolith (hybrid)
- •Choosing based on requirements
3. Interface Design
- •Module APIs and contracts
- •Event schemas
- •Data sharing strategies
- •Versioning and compatibility
4. Database Strategy
- •Shared database pattern
- •Database per module pattern
- •Hybrid database strategy
- •Data consistency approaches
Architecture Patterns
Pattern 1: Modular Monolith
When to Use:
- •Early-stage platforms
- •Small to medium teams
- •Shared domain context
- •Rapid iteration needed
- •Preparing for future microservices
Architecture:
┌─────────────────────────────────────────┐ │ Monolithic Application │ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │Module A│ │Module B│ │Module C│ │ │ │ │ │ │ │ │ │ │ │ Domain │ │ Domain │ │ Domain │ │ │ │ Logic │ │ Logic │ │ Logic │ │ │ └───┬────┘ └───┬────┘ └───┬────┘ │ │ └───────────┼───────────┘ │ │ │ │ │ ┌────────▼────────┐ │ │ │ Shared Database │ │ │ └─────────────────┘ │ └─────────────────────────────────────────┘
Directory Structure:
src/ ├── modules/ │ ├── knowledge-management/ │ │ ├── domain/ │ │ │ ├── entities/ │ │ │ ├── value-objects/ │ │ │ └── repositories/ │ │ ├── application/ │ │ │ ├── services/ │ │ │ └── use-cases/ │ │ ├── infrastructure/ │ │ │ └── persistence/ │ │ └── presentation/ │ │ └── api/ │ │ │ ├── security/ │ │ ├── domain/ │ │ ├── application/ │ │ ├── infrastructure/ │ │ └── presentation/ │ │ │ └── use-cases/ │ ├── domain/ │ ├── application/ │ ├── infrastructure/ │ └── presentation/ │ ├── shared/ │ ├── kernel/ │ ├── events/ │ └── infrastructure/ │ └── main.ts
Module Interface Example:
// Module: Knowledge Management
// Public API (what other modules can use)
export interface KnowledgeManagementAPI {
// Document operations
indexDocument(content: string, metadata: DocumentMetadata): Promise<string>;
searchDocuments(query: string): Promise<SearchResult[]>;
getDocument(id: string): Promise<Document | null>;
// Events published by this module
on(event: 'DocumentIndexed', handler: (doc: Document) => void): void;
on(event: 'DocumentUpdated', handler: (doc: Document) => void): void;
}
// Internal implementation (not exposed)
class KnowledgeManagementModule implements KnowledgeManagementAPI {
constructor(
private documentRepository: DocumentRepository,
private searchEngine: SearchEngine,
private eventBus: EventBus
) {}
async indexDocument(content: string, metadata: DocumentMetadata): Promise<string> {
// Internal logic not exposed
const document = await this.parseAndIndex(content, metadata);
this.eventBus.publish(new DocumentIndexedEvent(document));
return document.id;
}
// Other public methods...
// Private methods (internal to module)
private async parseAndIndex(content: string, metadata: DocumentMetadata) {
// Module-internal logic
}
}
// Module registration
export function registerKnowledgeManagementModule(container: DIContainer) {
container.register('KnowledgeManagementAPI', KnowledgeManagementModule);
}
Cross-Module Communication:
// Module: Use Cases (depends on Knowledge Management)
import { KnowledgeManagementAPI } from '../knowledge-management';
class DocumentAnalysisUseCase {
constructor(
private knowledgeAPI: KnowledgeManagementAPI // Dependency injection
) {}
async analyzeDocument(query: string) {
// Call Knowledge Management via interface
const documents = await this.knowledgeAPI.searchDocuments(query);
// Use Case logic...
return this.performAnalysis(documents);
}
}
Trade-offs:
- •✅ Simple deployment (single artifact)
- •✅ Easy refactoring (shared codebase)
- •✅ Transactions across modules
- •✅ Lower operational complexity
- •❌ Must deploy all modules together
- •❌ Shared scaling (cannot scale modules independently)
- •❌ Potential tight coupling if not disciplined
Pattern 2: Microservices
When to Use:
- •Large platforms (10+ modules)
- •Multiple teams with autonomy
- •Different scaling requirements per module
- •Technology diversity needed
- •Independent deployment critical
Architecture:
┌────────────┐ ┌────────────┐ ┌────────────┐
│ Module A │ │ Module B │ │ Module C │
│ Service │ │ Service │ │ Service │
│ │ │ │ │ │
│ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │
│ │ DB │ │ │ │ DB │ │ │ │ DB │ │
│ └──────┘ │ │ └──────┘ │ │ └──────┘ │
└──────┬─────┘ └─────┬──────┘ └─────┬──────┘
│ │ │
└─────────────────┼───────────────────┘
│
┌──────▼──────┐
│ API Gateway │
│ Event Bus │
└─────────────┘
Service Structure:
services/
├── knowledge-management/
│ ├── src/
│ │ ├── domain/
│ │ ├── application/
│ │ └── infrastructure/
│ ├── Dockerfile
│ ├── package.json
│ └── README.md
│
├── security/
│ ├── src/
│ ├── Dockerfile
│ └── package.json
│
└── use-cases/
├── src/
├── Dockerfile
└── package.json
Service Communication:
// Service: Knowledge Management
import express from 'express';
const app = express();
// REST API
app.get('/v1/documents/:id', async (req, res) => {
const document = await documentService.findById(req.params.id);
res.json(document);
});
app.post('/v1/documents', async (req, res) => {
const document = await documentService.index(req.body);
// Publish event to message bus
await messageBus.publish({
type: 'DocumentIndexed',
payload: document
});
res.status(201).json(document);
});
app.listen(3001);
// Service: Use Cases
import axios from 'axios';
class DocumentAnalysisService {
private knowledgeServiceURL = process.env.KNOWLEDGE_SERVICE_URL;
async analyzeDocument(documentId: string) {
// HTTP call to Knowledge Management service
const response = await axios.get(
`${this.knowledgeServiceURL}/v1/documents/${documentId}`
);
const document = response.data;
// Analysis logic...
return this.performAnalysis(document);
}
}
Service Discovery:
# docker-compose.yml
version: '3'
services:
knowledge-management:
build: ./services/knowledge-management
environment:
- DATABASE_URL=postgres://db/knowledge
ports:
- "3001:3000"
security:
build: ./services/security
environment:
- DATABASE_URL=postgres://db/security
ports:
- "3002:3000"
use-cases:
build: ./services/use-cases
environment:
- KNOWLEDGE_SERVICE_URL=http://knowledge-management:3000
- SECURITY_SERVICE_URL=http://security:3000
ports:
- "3003:3000"
Trade-offs:
- •✅ Independent deployment
- •✅ Independent scaling
- •✅ Technology diversity
- •✅ Team autonomy
- •❌ Operational complexity (many services)
- •❌ Distributed systems challenges
- •❌ No cross-service transactions
- •❌ Network latency
Pattern 3: Hybrid (Strategic Microservices)
When to Use:
- •Core modules in monolith
- •Performance-critical modules as services
- •Gradual migration from monolith
- •Balance simplicity with flexibility
Architecture:
┌─────────────────────────────────┐
│ Monolithic Core │
│ ┌─────────┐ ┌─────────┐ │
│ │Module A │ │Module B │ │
│ └─────────┘ └─────────┘ │
│ │ │ │
│ ┌────▼─────────────▼────┐ │
│ │ Shared Database │ │
│ └───────────────────────┘ │
└──────────┬──────────────────────┘
│
┌──────┼──────┐
│ │ │
┌───▼──┐ ┌─▼───┐ ┌▼────┐
│Svc C │ │Svc D│ │Svc E│
│┌────┐│ │┌───┐│ │┌───┐│
││ DB ││ ││DB ││ ││DB ││
│└────┘│ │└───┘│ │└───┘│
└──────┘ └─────┘ └─────┘
Example: Knowledge Management Platform
Monolithic Core: - Knowledge Management (core, shared by many) - Security (needed by all, low change rate) - Operations (admin features) Microservices: - ML Processing Service (CPU-intensive, scales independently) - Web Scraper Service (external integrations, failures isolated) - Real-time Chat Service (WebSocket-heavy, special scaling needs)
Trade-offs:
- •✅ Balance simplicity with flexibility
- •✅ Extract only what needs independence
- •✅ Gradual migration path
- •❌ Hybrid complexity (both monolith AND services)
- •❌ Need to manage both deployment models
Database Strategies
Strategy 1: Shared Database
Pattern:
┌─────────┐ ┌─────────┐ ┌─────────┐
│Module A │ │Module B │ │Module C │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└──────────────┼──────────────┘
│
┌───────▼────────┐
│ Shared Database│
│ │
│ ┌────────────┐ │
│ │Schema: A │ │
│ │Schema: B │ │
│ │Schema: C │ │
│ └────────────┘ │
└────────────────┘
Access Rules:
// Module A can ONLY access its own schema
class ModuleARepository {
async save(entity: EntityA) {
return db.query('INSERT INTO module_a.entities ...', [entity]);
}
// ❌ NEVER do this (accessing Module B schema)
// async getModuleBData() {
// return db.query('SELECT * FROM module_b.entities');
// }
}
// Module B can ONLY access its own schema
class ModuleBRepository {
async save(entity: EntityB) {
return db.query('INSERT INTO module_b.entities ...', [entity]);
}
}
Pros:
- •Simple transactions across modules
- •Easy joins for queries
- •Single backup/restore
Cons:
- •Schema coupling
- •Single scaling unit
- •Migration coordination needed
Strategy 2: Database per Module
Pattern:
┌─────────┐ ┌─────────┐ ┌─────────┐
│Module A │ │Module B │ │Module C │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ DB A │ │ DB B │ │ DB C │
└─────────┘ └─────────┘ └─────────┘
Data Sharing via APIs:
// Module A wants Module B data
// ❌ NEVER access Module B database directly
// const data = await moduleBDatabase.query('SELECT ...');
// ✅ Call Module B API
const data = await moduleBAPI.getData();
Eventual Consistency:
// Module A publishes event
await eventBus.publish({
type: 'OrderCreated',
payload: { orderId: '123', userId: 'user-456' }
});
// Module B subscribes and syncs its read model
eventBus.subscribe('OrderCreated', async (event) => {
await moduleBDatabase.query(`
INSERT INTO order_cache (order_id, user_id, created_at)
VALUES ($1, $2, $3)
`, [event.payload.orderId, event.payload.userId, new Date()]);
});
Pros:
- •True module independence
- •Different database technologies
- •Independent scaling
Cons:
- •No cross-module transactions
- •Eventual consistency
- •Data duplication
Module Interface Patterns
Pattern 1: Facade Pattern
// Module exposes single entry point
export class KnowledgeManagementFacade {
constructor(
private documentService: DocumentService,
private searchService: SearchService,
private indexingService: IndexingService
) {}
// Simplified API for other modules
async indexDocument(content: string, metadata: any): Promise<string> {
const parsed = await this.documentService.parse(content);
const indexed = await this.indexingService.index(parsed, metadata);
await this.searchService.addToIndex(indexed);
return indexed.id;
}
async search(query: string): Promise<SearchResult[]> {
return this.searchService.search(query);
}
}
Pattern 2: Repository Pattern
// Module exposes data access abstraction
export interface DocumentRepository {
findById(id: string): Promise<Document | null>;
findByQuery(query: Query): Promise<Document[]>;
save(document: Document): Promise<void>;
delete(id: string): Promise<void>;
}
// Implementation hidden inside module
class PostgresDocumentRepository implements DocumentRepository {
// Internal implementation
}
Pattern 3: Event-Driven Interface
// Module publishes domain events
export interface KnowledgeManagementEvents {
DocumentIndexed: DocumentIndexedEvent;
DocumentUpdated: DocumentUpdatedEvent;
DocumentDeleted: DocumentDeletedEvent;
}
// Other modules subscribe
eventBus.subscribe<DocumentIndexedEvent>('DocumentIndexed', (event) => {
console.log('Document indexed:', event.documentId);
});
Tools Required
Module Boundaries
- •Domain-Driven Design principles
- •Bounded Context mapping
- •Conway's Law considerations
Visualization
- •C4 Model diagrams
- •Module dependency graphs
Enforcement
- •Linters (enforce import rules)
- •Architecture tests
Quality Checklist
Module Design
- • Clear module purpose (Single Responsibility)
- • Well-defined public interface
- • Internal implementation hidden
- • Minimal dependencies on other modules
Communication
- • Interface contracts documented
- • Versioning strategy defined
- • Error handling specified
- • Events schema documented
Database
- • Database strategy chosen (shared vs per-module)
- • Access rules enforced
- • Migration strategy defined
Anti-Patterns to Avoid
- •Distributed Monolith: Microservices with tight coupling
- •Leaky Abstraction: Internal details exposed in interface
- •God Module: One module with too many responsibilities
- •Bypass APIs: Direct database access across modules
- •Shared Models: Domain models leaked across module boundaries
Remember: Modularity is about managing complexity through clear boundaries, not creating complexity through unnecessary distribution. Start simple, evolve as needed.