A production-ready e-commerce platform built as a modular monolith using domain-driven design principles. This architecture prevents the "Big Ball of Mud" anti-pattern while maintaining the operational simplicity of a monolithic deployment.
- Architecture Overview
- What is a Modular Monolith?
- Domain Boundaries
- Module Dependency Rules
- Technology Stack
- Project Structure
- Getting Started
- Database Schema
- API Documentation
- Development Guidelines
- Testing Strategy
- Deployment
- Migration Path
This project implements a modular monolith architecture with clear module boundaries and strict dependency rules. Unlike microservices, all modules run in a single process and share a database, but maintain logical separation through well-defined interfaces.
- Separate Backend API Server: Business logic resides in Express.js, not Next.js API routes
- Module Boundaries: Each module owns its domain logic, data models, and data access layer
- No Circular Dependencies: Enforced through strict import rules and architectural tests
- Single Database: All modules share PostgreSQL with Prisma ORM
- REST APIs Only: No GraphQL to maintain simplicity
A modular monolith is a software architecture pattern where:
- The application is deployed as a single unit (monolith)
- Code is organized into independent modules with clear boundaries
- Modules communicate through well-defined interfaces
- Each module has high cohesion and low coupling
| Aspect | Traditional Monolith | Modular Monolith | Microservices |
|---|---|---|---|
| Deployment | Single unit | Single unit | Multiple services |
| Database | Shared | Shared | Separate per service |
| Module Boundaries | Weak/none | Strong | Very strong |
| Communication | Direct calls | Interface calls | Network (HTTP/gRPC) |
| Complexity | Low | Medium | High |
| Team Autonomy | Low | Medium | High |
| Operational Overhead | Low | Low | High |
| Scalability | Vertical | Vertical | Horizontal |
✅ Simplicity: Single deployment, single database, simpler operations
✅ Performance: No network latency between modules
✅ ACID Transactions: Database transactions span multiple modules
✅ Refactoring: Easy to move code between modules
✅ Testing: Integration tests are straightforward
✅ Migration Path: Can extract modules to microservices later
Our e-commerce platform is divided into four core modules, each with clear responsibilities:
Responsibility: User account management and profile data
Owns:
- User registration and profile management
- User authentication (future)
- User preferences and settings
Data Models:
User
Dependencies: None (standalone module)
API Endpoints:
POST /api/users- Create userGET /api/users/:id- Get user by IDPATCH /api/users/:id- Update userDELETE /api/users/:id- Delete userGET /api/users- List users (paginated)
Responsibility: Product catalog, categories, variants, and inventory
Owns:
- Product and category management
- Product variant (SKU) management
- Inventory tracking and updates
- Product search and filtering
Data Models:
CategoryProductProductVariant
Dependencies: None (standalone module)
Business Rules:
- Products must belong to a category
- Variants represent distinct SKUs
- Inventory tracked at variant level only
- Inventory cannot be negative
API Endpoints:
POST /api/catalog/categories- Create categoryGET /api/catalog/categories- List categoriesPOST /api/catalog/products- Create productGET /api/catalog/products/:id- Get product with variantsPOST /api/catalog/variants- Create product variantPATCH /api/catalog/variants/:sku/inventory- Update inventory
Responsibility: Shopping cart management
Owns:
- Cart creation and management
- Adding/removing items from cart
- Cart item quantity updates
- Cart summary calculations
Data Models:
CartCartItem
Dependencies:
- Catalog Module: To validate product variants and check inventory
Business Rules:
- Each user has one active cart
- Cart items reference
ProductVariant(notProduct) - Quantity must not exceed available inventory
- Cart items are unique per variant (no duplicates)
API Endpoints:
GET /api/cart/:userId- Get or create cart for userPOST /api/cart/items- Add item to cartPATCH /api/cart/items/:itemId- Update cart item quantityDELETE /api/cart/items/:itemId- Remove item from cartDELETE /api/cart/:cartId/clear- Clear entire cart
Responsibility: Order processing and order history
Owns:
- Order creation from cart
- Order status management
- Order history and tracking
- Inventory deduction on order placement
Data Models:
OrderOrderLineItem
Dependencies:
- Cart Module: To retrieve cart items for order creation
- Catalog Module: To validate inventory and decrement stock
Business Rules:
- Orders are immutable snapshots of cart at purchase time
OrderLineItemstores product/variant names and prices (no foreign keys)- Inventory decremented atomically during order creation
- Order creation and inventory update happen in a database transaction
- Empty carts cannot be converted to orders
API Endpoints:
POST /api/orders- Create order from cartGET /api/orders/:id- Get order detailsPATCH /api/orders/:id/status- Update order statusGET /api/orders/user/:userId- List user's ordersGET /api/orders- List all orders (admin)
┌─────────────────────────────────────────────────────────┐
│ Application Layer │
│ (app.ts, server.ts) │
└─────────────────────────────────────────────────────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ User Module │ │ Cart Module │ │ Order Module │
│ (Standalone) │ │ │ │ │
└──────────────┘ └──────┬───────┘ └──────┬───────┘
│ │
│ ┌────────┴────────┐
│ │ │
▼ ▼ ▼
┌──────────────────────┐ ┌──────────────┐
│ Catalog Module │ │ Cart Module │
│ (Standalone) │ │ │
└──────────────────────┘ └──────────────┘
| Module | Can Depend On |
|---|---|
| User | None |
| Catalog | None |
| Cart | Catalog |
| Order | Cart, Catalog |
❌ Circular Dependencies: No module can depend on a module that depends on it
❌ User Dependencies: No module should depend on User (authentication will be handled separately)
❌ Catalog → Cart: Catalog cannot import from Cart
❌ Catalog → Order: Catalog cannot import from Order
❌ Cart → Order: Cart cannot import from Order
Manual Review:
# Check imports in catalog module - should NOT import from cart or order
grep -r "from.*cart" backend/src/modules/catalog/
grep -r "from.*order" backend/src/modules/catalog/Future: ESLint Plugin (recommended):
// .eslintrc.js (future implementation)
{
"rules": {
"import/no-restricted-paths": [
"error",
{
"zones": [
{
"target": "./src/modules/catalog",
"from": "./src/modules/cart"
},
{
"target": "./src/modules/catalog",
"from": "./src/modules/order"
},
{
"target": "./src/modules/cart",
"from": "./src/modules/order"
}
]
}
]
}
}Future: Architecture Tests:
// tests/architecture.test.ts
describe('Module Dependencies', () => {
it('catalog should not depend on cart or order', () => {
// Use dependency-cruiser or madge to verify
});
});- Framework: Next.js 15+ (App Router)
- Language: TypeScript (strict mode)
- Styling: TailwindCSS + shadcn/ui
- State Management: React Context / Zustand (no Redux)
- Runtime: Node.js 20 LTS
- Framework: Express.js 4.x
- Language: TypeScript 5.x (strict mode)
- ORM: Prisma 5.x
- Database: PostgreSQL 16+
- Package Manager: npm / pnpm / yarn
- Build Tool: TypeScript Compiler (tsc)
- Dev Server: tsx (TypeScript Execute)
- Code Quality: ESLint + Prettier
ecommerce-modular-monolith/
│
├── backend/ # Express.js API Server
│ ├── src/
│ │ ├── modules/ # Domain Modules
│ │ │ ├── user/
│ │ │ │ ├── user.controller.ts # HTTP routes
│ │ │ │ ├── user.service.ts # Business logic
│ │ │ │ ├── user.repository.ts # Data access
│ │ │ │ ├── types.ts # DTOs and interfaces
│ │ │ │ └── index.ts # Public API
│ │ │ │
│ │ │ ├── catalog/
│ │ │ │ ├── catalog.controller.ts
│ │ │ │ ├── catalog.service.ts
│ │ │ │ ├── catalog.repository.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── index.ts
│ │ │ │
│ │ │ ├── cart/
│ │ │ │ ├── cart.controller.ts
│ │ │ │ ├── cart.service.ts
│ │ │ │ ├── cart.repository.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── index.ts
│ │ │ │
│ │ │ └── order/
│ │ │ ├── order.controller.ts
│ │ │ ├── order.service.ts
│ │ │ ├── order.repository.ts
│ │ │ ├── types.ts
│ │ │ └── index.ts
│ │ │
│ │ ├── shared/ # Shared Infrastructure
│ │ │ ├── db/
│ │ │ │ ├── prisma.ts # Prisma client
│ │ │ │ └── types.ts # Shared DB types
│ │ │ │
│ │ │ ├── config/
│ │ │ │ ├── index.ts # Configuration
│ │ │ │ └── constants.ts # Constants
│ │ │ │
│ │ │ ├── middleware/
│ │ │ │ ├── errorHandler.ts # Error handling
│ │ │ │ ├── validation.ts # Request validation
│ │ │ │ └── logger.ts # Request logging
│ │ │ │
│ │ │ └── utils/
│ │ │ ├── apiResponse.ts # API response helpers
│ │ │ └── validation.ts # Validation utilities
│ │ │
│ │ ├── app.ts # Express app setup
│ │ └── server.ts # Server entry point
│ │
│ ├── prisma/
│ │ ├── schema.prisma # Database schema
│ │ └── migrations/ # Database migrations
│ │
│ ├── package.json
│ ├── tsconfig.json
│ ├── .env.example
│ └── .gitignore
│
├── frontend/ # Next.js App (Future)
│ └── (to be implemented)
│
├── STACK_DECISION.md # Architecture decisions
└── README.md # This file
Each module follows a consistent three-layer architecture:
module/
├── controller.ts # HTTP layer (routes, request/response)
├── service.ts # Business logic layer
├── repository.ts # Data access layer (Prisma queries)
├── types.ts # TypeScript types and DTOs
└── index.ts # Public exports
Layer Responsibilities:
- Controller: Handle HTTP requests, validate input, call service, format response
- Service: Implement business rules, orchestrate repositories, handle transactions
- Repository: Execute database queries, abstract Prisma operations
- Types: Define interfaces, DTOs, and domain models
- Node.js 20+ LTS
- PostgreSQL 16+
- npm / pnpm / yarn
- Clone the repository:
git clone <repository-url>
cd ecommerce-modular-monolith- Install backend dependencies:
cd backend
npm install- Set up environment variables:
cp .env.example .env
# Edit .env with your database credentials- Set up the database:
# Create PostgreSQL database
createdb ecommerce_db
# Run Prisma migrations
npm run prisma:migrate
# Generate Prisma Client
npm run prisma:generate- Start the development server:
npm run devThe API server will start on http://localhost:4000
# Health check
curl http://localhost:4000/health
# Expected response:
# {"status":"ok","timestamp":"2026-01-24T..."}┌─────────────┐
│ User │
└──────┬──────┘
│
│ 1:N
│
┌──────▼──────┐ ┌──────────────┐
│ Cart │────────▶│ CartItem │
└─────────────┘ 1:N └──────┬───────┘
│
┌───────────────────────┘
│ N:1
│
┌──────▼──────────┐ ┌─────────────┐
│ ProductVariant │◀─────│ Product │
└─────────────────┘ N:1 └──────┬──────┘
│ N:1
│
┌──────▼──────┐
│ Category │
└─────────────┘
┌─────────────┐
│ User │
└──────┬──────┘
│ 1:N
│
┌──────▼──────┐ ┌──────────────────┐
│ Order │────────▶│ OrderLineItem │
└─────────────┘ 1:N └──────────────────┘
(No FK to Product/Variant)
Immutable Orders:
OrderLineItemstores snapshots (productName, variantName, priceAtPurchase)- No foreign keys to
ProductorProductVariant - Order history preserved even if products are deleted
Inventory Management:
- Inventory tracked at
ProductVariantlevel only - Direct storage (not computed)
- Decremented atomically during order creation
Unique Constraints:
ProductVariant.sku- Globally uniqueProduct.slug- Globally uniqueCategory.slug- Globally uniqueUser.email- Globally uniqueCartItem(cartId, productVariantId)- Composite unique
http://localhost:4000/api
All API responses follow this structure:
Success Response:
{
"success": true,
"data": { ... }
}Error Response:
{
"success": false,
"error": {
"message": "Error description",
"code": "ERROR_CODE",
"details": { ... }
}
}Paginated Response:
{
"success": true,
"data": [ ... ],
"meta": {
"page": 1,
"limit": 20,
"total": 100,
"totalPages": 5
}
}Create a Product:
curl -X POST http://localhost:4000/api/catalog/products \
-H "Content-Type: application/json" \
-d '{
"name": "Wireless Mouse",
"slug": "wireless-mouse",
"description": "Ergonomic wireless mouse",
"categoryId": "cat_123",
"isActive": true
}'Add Item to Cart:
curl -X POST http://localhost:4000/api/cart/items \
-H "Content-Type: application/json" \
-d '{
"cartId": "cart_456",
"productVariantId": "var_789",
"quantity": 2
}'Create Order:
curl -X POST http://localhost:4000/api/orders \
-H "Content-Type: application/json" \
-d '{
"userId": "user_123",
"cartId": "cart_456"
}'- Single Responsibility: Each module handles one domain area
- Dependency Direction: Only import from allowed modules (see dependency rules)
- No Shared State: Modules communicate through service interfaces
- Explicit Exports: Only export through
index.ts
DO:
// ✅ Import from module's public API
import { CatalogService } from '../catalog';
// ✅ Keep business logic in service layer
class CartService {
async addItem(data: AddCartItemDTO) {
// Business logic here
}
}DON'T:
// ❌ Don't import internal files from other modules
import { CatalogRepository } from '../catalog/catalog.repository';
// ❌ Don't put business logic in controllers
class CartController {
async addItem(req, res) {
// Business logic should be in service
const variant = await prisma.productVariant.findUnique(...);
}
}- Create module directory:
src/modules/new-module/ - Implement layers: controller, service, repository, types
- Export public API through
index.ts - Update
app.tsto register routes - Update dependency documentation
- Add Prisma models if needed
# Create a new migration
npm run prisma:migrate
# View database in Prisma Studio
npm run prisma:studio
# Reset database (development only)
npx prisma migrate reset ┌─────────────┐
│ E2E Tests │ (10%)
└─────────────┘
┌─────────────────┐
│ Integration Tests│ (30%)
└─────────────────┘
┌─────────────────────┐
│ Unit Tests │ (60%)
└─────────────────────┘
Test individual functions in isolation:
// user.service.test.ts
describe('UserService', () => {
it('should create a user with valid email', async () => {
const service = new UserService();
const user = await service.createUser({
email: 'test@example.com',
firstName: 'John',
lastName: 'Doe'
});
expect(user.email).toBe('test@example.com');
});
});Test module interactions:
// order.integration.test.ts
describe('Order Creation', () => {
it('should create order and decrement inventory', async () => {
// Add item to cart
// Create order
// Verify inventory decreased
});
});Test complete user flows (future implementation with Playwright).
Required environment variables:
PORT=4000
NODE_ENV=production
DATABASE_URL=postgresql://user:pass@host:5432/db
CORS_ORIGIN=https://yourdomain.com# Build TypeScript
npm run build
# Start production server
npm startFROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist ./dist
EXPOSE 4000
CMD ["node", "dist/server.js"]Vertical Scaling: Increase server resources (CPU, RAM)
Horizontal Scaling: Run multiple instances behind load balancer
Database: Use connection pooling, read replicas
Caching: Add Redis for frequently accessed data
Consider extracting a module to a microservice when:
- Independent Scaling: Module needs 10x more resources than others
- Team Autonomy: Team needs to deploy independently multiple times per day
- Technology Requirements: Module needs different tech stack (e.g., Python for ML)
- Regulatory Isolation: Compliance requires physical separation (e.g., PCI-DSS)
- Identify Module: Choose module with clear boundaries (e.g., Catalog)
- Create Service: Set up new service with own database
- Implement API: Expose module functionality via REST API
- Update Clients: Change imports to HTTP calls
- Migrate Data: Copy relevant data to new database
- Deploy: Deploy service independently
- Remove Module: Delete module from monolith
Before (Modular Monolith):
// cart.service.ts
import { CatalogService } from '../catalog';
const variant = await this.catalogService.getProductVariantBySku(sku);After (Microservice):
// cart.service.ts
import axios from 'axios';
const response = await axios.get(`${CATALOG_SERVICE_URL}/variants/${sku}`);
const variant = response.data;- Follow the module structure pattern
- Respect dependency rules (no circular dependencies)
- Write tests for new features
- Update documentation
- Run linter before committing
MIT
For questions or issues, please open a GitHub issue or contact the development team.
Built with ❤️ using Domain-Driven Design and Modular Monolith Architecture