Skip to content

abdulhaqcode/ecommerce-modular-monolith

Repository files navigation

E-Commerce Modular Monolith

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.


Table of Contents


Architecture Overview

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.

Key Architectural Decisions

  1. Separate Backend API Server: Business logic resides in Express.js, not Next.js API routes
  2. Module Boundaries: Each module owns its domain logic, data models, and data access layer
  3. No Circular Dependencies: Enforced through strict import rules and architectural tests
  4. Single Database: All modules share PostgreSQL with Prisma ORM
  5. REST APIs Only: No GraphQL to maintain simplicity

What is a Modular Monolith?

Definition

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

Comparison with Other Architectures

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

Benefits of Modular Monolith

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

Trade-offs

⚠️ Scaling: Must scale entire application, not individual modules
⚠️ Technology Lock-in: All modules use same tech stack
⚠️ Deployment Coupling: Changes to any module require full deployment
⚠️ Team Coordination: Shared codebase requires coordination


Domain Boundaries

Our e-commerce platform is divided into four core modules, each with clear responsibilities:

1. User Module 👤

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 user
  • GET /api/users/:id - Get user by ID
  • PATCH /api/users/:id - Update user
  • DELETE /api/users/:id - Delete user
  • GET /api/users - List users (paginated)

2. Catalog Module 📦

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:

  • Category
  • Product
  • ProductVariant

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 category
  • GET /api/catalog/categories - List categories
  • POST /api/catalog/products - Create product
  • GET /api/catalog/products/:id - Get product with variants
  • POST /api/catalog/variants - Create product variant
  • PATCH /api/catalog/variants/:sku/inventory - Update inventory

3. Cart Module 🛒

Responsibility: Shopping cart management

Owns:

  • Cart creation and management
  • Adding/removing items from cart
  • Cart item quantity updates
  • Cart summary calculations

Data Models:

  • Cart
  • CartItem

Dependencies:

  • Catalog Module: To validate product variants and check inventory

Business Rules:

  • Each user has one active cart
  • Cart items reference ProductVariant (not Product)
  • 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 user
  • POST /api/cart/items - Add item to cart
  • PATCH /api/cart/items/:itemId - Update cart item quantity
  • DELETE /api/cart/items/:itemId - Remove item from cart
  • DELETE /api/cart/:cartId/clear - Clear entire cart

4. Order Module 📋

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:

  • Order
  • OrderLineItem

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
  • OrderLineItem stores 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 cart
  • GET /api/orders/:id - Get order details
  • PATCH /api/orders/:id/status - Update order status
  • GET /api/orders/user/:userId - List user's orders
  • GET /api/orders - List all orders (admin)

Module Dependency Rules

Dependency Hierarchy

┌─────────────────────────────────────────────────────────┐
│                    Application Layer                     │
│                  (app.ts, server.ts)                     │
└─────────────────────────────────────────────────────────┘
                           │
        ┌──────────────────┼──────────────────┐
        ▼                  ▼                  ▼
┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│ User Module  │  │ Cart Module  │  │ Order Module │
│ (Standalone) │  │              │  │              │
└──────────────┘  └──────┬───────┘  └──────┬───────┘
                         │                  │
                         │         ┌────────┴────────┐
                         │         │                 │
                         ▼         ▼                 ▼
                  ┌──────────────────────┐  ┌──────────────┐
                  │   Catalog Module     │  │ Cart Module  │
                  │    (Standalone)      │  │              │
                  └──────────────────────┘  └──────────────┘

Allowed Dependencies

Module Can Depend On
User None
Catalog None
Cart Catalog
Order Cart, Catalog

Forbidden Dependencies

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

Enforcing Dependencies

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
  });
});

Technology Stack

Frontend (Future Implementation)

  • Framework: Next.js 15+ (App Router)
  • Language: TypeScript (strict mode)
  • Styling: TailwindCSS + shadcn/ui
  • State Management: React Context / Zustand (no Redux)

Backend (Current Implementation)

  • Runtime: Node.js 20 LTS
  • Framework: Express.js 4.x
  • Language: TypeScript 5.x (strict mode)
  • ORM: Prisma 5.x
  • Database: PostgreSQL 16+

Development Tools

  • Package Manager: npm / pnpm / yarn
  • Build Tool: TypeScript Compiler (tsc)
  • Dev Server: tsx (TypeScript Execute)
  • Code Quality: ESLint + Prettier

Project Structure

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

Module Structure Pattern

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:

  1. Controller: Handle HTTP requests, validate input, call service, format response
  2. Service: Implement business rules, orchestrate repositories, handle transactions
  3. Repository: Execute database queries, abstract Prisma operations
  4. Types: Define interfaces, DTOs, and domain models

Getting Started

Prerequisites

  • Node.js 20+ LTS
  • PostgreSQL 16+
  • npm / pnpm / yarn

Installation

  1. Clone the repository:
git clone <repository-url>
cd ecommerce-modular-monolith
  1. Install backend dependencies:
cd backend
npm install
  1. Set up environment variables:
cp .env.example .env
# Edit .env with your database credentials
  1. Set up the database:
# Create PostgreSQL database
createdb ecommerce_db

# Run Prisma migrations
npm run prisma:migrate

# Generate Prisma Client
npm run prisma:generate
  1. Start the development server:
npm run dev

The API server will start on http://localhost:4000

Verify Installation

# Health check
curl http://localhost:4000/health

# Expected response:
# {"status":"ok","timestamp":"2026-01-24T..."}

Database Schema

Entity Relationship Diagram

┌─────────────┐
│    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)

Key Schema Features

Immutable Orders:

  • OrderLineItem stores snapshots (productName, variantName, priceAtPurchase)
  • No foreign keys to Product or ProductVariant
  • Order history preserved even if products are deleted

Inventory Management:

  • Inventory tracked at ProductVariant level only
  • Direct storage (not computed)
  • Decremented atomically during order creation

Unique Constraints:

  • ProductVariant.sku - Globally unique
  • Product.slug - Globally unique
  • Category.slug - Globally unique
  • User.email - Globally unique
  • CartItem(cartId, productVariantId) - Composite unique

API Documentation

Base URL

http://localhost:4000/api

Response Format

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
  }
}

Example API Calls

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"
  }'

Development Guidelines

Module Development Rules

  1. Single Responsibility: Each module handles one domain area
  2. Dependency Direction: Only import from allowed modules (see dependency rules)
  3. No Shared State: Modules communicate through service interfaces
  4. Explicit Exports: Only export through index.ts

Code Organization

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(...);
  }
}

Adding a New Module

  1. Create module directory: src/modules/new-module/
  2. Implement layers: controller, service, repository, types
  3. Export public API through index.ts
  4. Update app.ts to register routes
  5. Update dependency documentation
  6. Add Prisma models if needed

Database Migrations

# 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

Testing Strategy

Test Pyramid

        ┌─────────────┐
        │   E2E Tests │  (10%)
        └─────────────┘
      ┌─────────────────┐
      │ Integration Tests│  (30%)
      └─────────────────┘
    ┌─────────────────────┐
    │    Unit Tests       │  (60%)
    └─────────────────────┘

Unit Tests

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');
  });
});

Integration Tests

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
  });
});

E2E Tests

Test complete user flows (future implementation with Playwright).


Deployment

Environment Variables

Required environment variables:

PORT=4000
NODE_ENV=production
DATABASE_URL=postgresql://user:pass@host:5432/db
CORS_ORIGIN=https://yourdomain.com

Production Build

# Build TypeScript
npm run build

# Start production server
npm start

Docker Deployment (Future)

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist ./dist
EXPOSE 4000
CMD ["node", "dist/server.js"]

Scaling Considerations

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


Migration Path

When to Extract Microservices

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)

Extraction Process

  1. Identify Module: Choose module with clear boundaries (e.g., Catalog)
  2. Create Service: Set up new service with own database
  3. Implement API: Expose module functionality via REST API
  4. Update Clients: Change imports to HTTP calls
  5. Migrate Data: Copy relevant data to new database
  6. Deploy: Deploy service independently
  7. Remove Module: Delete module from monolith

Example: Extracting Catalog Module

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;

Contributing

  1. Follow the module structure pattern
  2. Respect dependency rules (no circular dependencies)
  3. Write tests for new features
  4. Update documentation
  5. Run linter before committing

License

MIT


Support

For questions or issues, please open a GitHub issue or contact the development team.


Built with ❤️ using Domain-Driven Design and Modular Monolith Architecture

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors