A modular monolith demonstrating Domain-Driven Design, event-driven architecture, and clean separation of concerns for an e-commerce order management system.
Features • Quick Start • Documentation • Contributing • Research
Tiny Store is a research project implementing a complete order lifecycle with:
- 4 bounded contexts (Orders, Inventory, Payments, Shipments)
- Event-driven communication between modules
- Rich domain models with business logic
- 310+ tests including architecture enforcement
- Complete REST API for all operations
graph TB
subgraph "Tiny Store"
API[API Layer<br/>Next.js]
subgraph "Bounded Contexts"
Orders[Orders Module<br/>State Machine]
Inventory[Inventory Module<br/>Stock Management]
Payments[Payments Module<br/>Processing]
Shipments[Shipments Module<br/>Tracking]
end
subgraph "Shared"
Domain[Shared Domain<br/>Money, Address]
Infra[Infrastructure<br/>EventBus, Database]
end
API --> Orders
API --> Inventory
API --> Payments
API --> Shipments
Orders -.event.-> Inventory
Inventory -.event.-> Orders
Orders -.event.-> Payments
Payments -.event.-> Orders
Orders -.event.-> Shipments
Orders --> Domain
Inventory --> Domain
Payments --> Domain
Shipments --> Domain
Orders --> Infra
Inventory --> Infra
Payments --> Infra
Shipments --> Infra
end
style Orders fill:#e1f5ff
style Inventory fill:#fff4e1
style Payments fill:#e8f5e9
style Shipments fill:#f3e5f5
- Node.js 18+
- npm
# Clone and install
git clone <repository>
cd tiny-store
npm install# Development server
npm run dev
# Production build
npm run buildServer runs at: http://localhost:3000
# All tests
npm test
# Specific categories
npm run test:unit # Domain logic tests
npm run test:boundary # Architecture enforcement
npm run test:integration # Event flow tests
# E2E tests (requires server running)
npm run dev # Terminal 1
npm run test:e2e:api # Terminal 2 - API tests
npm run test:e2e:perf # Terminal 2 - Performance tests
# Coverage
npm run test:coveragetiny-store/
├── apps/api/ # HTTP API (Next.js 15)
│ └── src/app/
│ ├── api/ # REST endpoints
│ │ ├── orders/
│ │ ├── inventory/
│ │ └── events/
│ └── lib/ # Infrastructure setup
│ ├── database.ts # TypeORM connection
│ ├── event-bus.ts # Event bus singleton
│ └── register-listeners.ts # Wire event handlers
│
├── libs/modules/ # Bounded contexts
│ ├── orders/
│ │ └── src/
│ │ ├── domain/
│ │ │ ├── entities/ # Order aggregate
│ │ │ ├── value-objects/ # OrderItem, CustomerId
│ │ │ ├── events/ # 7 domain events
│ │ │ └── repositories/ # OrderRepository
│ │ ├── features/ # Use cases (vertical slices)
│ │ │ ├── place-order/
│ │ │ ├── get-order/
│ │ │ ├── list-orders/
│ │ │ └── cancel-order/
│ │ └── listeners/ # Event handlers
│ ├── inventory/ # Similar structure
│ ├── payments/ # Similar structure
│ └── shipments/ # Similar structure
│
├── libs/shared/
│ ├── domain/ # Base classes
│ │ ├── base/ # Entity, AggregateRoot
│ │ ├── value-objects/ # Money, Address
│ │ ├── errors/ # Domain errors
│ │ └── result/ # Result<T, E> type
│ ├── infrastructure/ # Shared infrastructure
│ │ ├── event-bus/ # In-memory pub/sub
│ │ ├── event-store/ # Append-only log
│ │ └── database/ # TypeORM setup
│ └── testing/ # Test utilities
│ └── src/
│ ├── test-helpers.ts # TestDatabase, EventBusSpy
│ ├── module-boundary.spec.ts # Architecture tests
│ └── event-flow.integration.spec.ts
│
├── docs/
│ ├── ARCHITECTURE.md # Design decisions
│ ├── API.md # Endpoint reference
│ ├── EVENT_FLOWS.md # Event choreography
│ └── TESTING.md # Testing guide
│
└── test-api.js # Standalone E2E tests
stateDiagram-v2
[*] --> PENDING: Order Placed
PENDING --> CONFIRMED: Inventory Reserved
PENDING --> REJECTED: Insufficient Stock
CONFIRMED --> PAID: Payment Success
CONFIRMED --> PAYMENT_FAILED: Payment Failed
PAID --> SHIPPED: Shipment Created
PENDING --> CANCELLED: User Cancels
CONFIRMED --> CANCELLED: User Cancels
PAID --> CANCELLED: User Cancels
REJECTED --> [*]
PAYMENT_FAILED --> [*]
SHIPPED --> [*]
CANCELLED --> [*]
State Transitions:
- PENDING - Order created, awaiting inventory check
- CONFIRMED - Inventory reserved, ready for payment
- REJECTED - Insufficient stock
- PAID - Payment successful
- PAYMENT_FAILED - Payment declined
- SHIPPED - Shipment created and dispatched
- CANCELLED - User cancelled order
Complete happy path from order to shipment:
sequenceDiagram
participant Client
participant API
participant Orders
participant Inventory
participant Payments
participant Shipments
Client->>API: POST /orders
API->>Orders: PlaceOrderHandler
Orders->>Orders: Create Order (PENDING)
Orders-->>EventBus: OrderPlaced
EventBus-->>Inventory: OrderPlaced
Inventory->>Inventory: Reserve Stock
Inventory-->>EventBus: InventoryReserved
EventBus-->>Orders: InventoryReserved
Orders->>Orders: Confirm (CONFIRMED)
Orders-->>EventBus: OrderConfirmed
EventBus-->>Payments: OrderConfirmed
Payments->>Payments: Process Payment
Payments-->>EventBus: PaymentProcessed
EventBus-->>Orders: PaymentProcessed
Orders->>Orders: Mark Paid (PAID)
Orders-->>EventBus: OrderPaid
EventBus-->>Shipments: OrderPaid
Shipments->>Shipments: Create Shipment
Shipments-->>EventBus: ShipmentCreated
EventBus-->>Orders: ShipmentCreated
Orders->>Orders: Mark Shipped (SHIPPED)
Client->>API: GET /orders/:id
API-->>Client: status: SHIPPED
Modules communicate only through events:
// ✅ Allowed
import { PlaceOrderHandler } from '@tiny-store/modules-orders';
eventBus.subscribe('OrderPlaced', handler);
// ❌ Blocked by architecture
import { Order } from '@tiny-store/modules-orders'; // Entity not exported
import { OrderRepository } from '@tiny-store/modules-orders'; // Not exportedEnforced by: 20 boundary tests verify architectural rules.
Business logic lives in domain entities, not controllers:
class Order extends AggregateRoot {
confirm(): void {
if (this.status !== OrderStatus.PENDING) {
throw new BusinessRuleViolationError('Only pending orders can be confirmed');
}
this.status = OrderStatus.CONFIRMED;
this.addDomainEvent(createOrderConfirmedEvent(this.id));
}
cancel(): void {
if (this.status === OrderStatus.SHIPPED) {
throw new BusinessRuleViolationError('Cannot cancel shipped orders');
}
this.status = OrderStatus.CANCELLED;
this.addDomainEvent(createOrderCancelledEvent(this.id));
}
}13 event types orchestrate the system:
// Orders publishes
eventBus.publish(createOrderPlacedEvent(order));
// Inventory listens and reacts
eventBus.subscribe('OrderPlaced', async (event) => {
await reserveStockHandler.handle(event);
});Result type for functional error handling:
const result = Money.create(100, 'USD');
if (result.isSuccess()) {
const money = result.getValue();
console.log(money.amount); // 100
} else {
const error = result.getError();
console.log(error.message); // "Invalid currency"
}When working with this modular monolith, avoid common mistakes that break architectural boundaries. Here's a key example:
WRONG - Directly calling infrastructure services from domain modules:
// ❌ WRONG - Directly calling mailing infrastructure from Order module
import { EmailService } from '@tiny-store/shared-infrastructure';
class OrderService {
constructor(
private emailService: EmailService // Direct infrastructure dependency!
) {}
async confirmOrder(orderId: string) {
const order = await this.orderRepository.findById(orderId);
order.confirm();
await this.orderRepository.save(order);
// Directly calling email infrastructure in Order module!
await this.emailService.send({
to: order.customerEmail,
subject: 'Order Confirmed',
body: `Your order ${order.id} has been confirmed.`
});
}
}CORRECT - Notifications module handles emails via events:
// ✅ CORRECT - Notifications module handles emails via events
import { EventBus } from '@tiny-store/shared-infrastructure';
class OrderService {
constructor(
private eventBus: EventBus // Events only!
) {}
async confirmOrder(orderId: string) {
const order = await this.orderRepository.findById(orderId);
order.confirm();
await this.orderRepository.save(order);
// Publish event - notifications module will handle email
this.eventBus.publish(new OrderConfirmedEvent({
orderId: order.id,
customerEmail: order.customerEmail,
total: order.total
}));
}
}
// Notifications module listens and handles email
class OrderConfirmedListener {
constructor(
private emailService: EmailService // Infrastructure only in notifications module
) {}
async handle(event: OrderConfirmedEvent) {
await this.emailService.send({
to: event.customerEmail,
subject: 'Order Confirmed',
body: `Your order ${event.orderId} has been confirmed.`
});
}
}See docs/ANTI_PATTERNS.md for a complete list of anti-patterns with detailed examples.
curl -X POST http://localhost:3000/api/inventory/products \
-H "Content-Type: application/json" \
-d '{
"sku": "WIDGET-001",
"name": "Super Widget",
"price": 99.99,
"currency": "USD",
"stockQuantity": 100
}'curl -X POST http://localhost:3000/api/orders \
-H "Content-Type: application/json" \
-d '{
"customerId": "customer-123",
"items": [{
"sku": "WIDGET-001",
"quantity": 2,
"unitPrice": 99.99,
"currency": "USD"
}],
"shippingAddress": {
"street": "123 Main St",
"city": "Springfield",
"state": "IL",
"postalCode": "62701",
"country": "USA"
}
}'# Get specific order
curl http://localhost:3000/api/orders/{orderId}
# List all orders
curl http://localhost:3000/api/orders
# Filter by status
curl http://localhost:3000/api/orders?status=PAID# All events
curl http://localhost:3000/api/events
# Events for specific order
curl http://localhost:3000/api/events?aggregateId={orderId}
# Specific event type
curl http://localhost:3000/api/events?eventType=OrderPlacedSee docs/API.md for complete API reference.
| Category | Tests | Purpose |
|---|---|---|
| Unit Tests | ~150 | Domain logic validation |
| Boundary Tests | 20 | Architecture enforcement |
| Integration Tests | 35 | Event flow scenarios |
| API Tests | 80+ | HTTP endpoint coverage |
| Performance Tests | 25 | Load & concurrency |
| TOTAL | 310+ | Complete coverage |
Unit Test - Domain Logic:
it('should not allow cancellation after shipment', () => {
const order = Order.create(/* ... */);
order.markAsShipped();
expect(() => order.cancel()).toThrow(BusinessRuleViolationError);
});Boundary Test - Architecture:
it('should NOT allow Orders module to import Inventory entities', () => {
let importFailed = false;
try {
const inventory = require('@tiny-store/modules-inventory');
const Product = inventory.Product; // Should not exist
importFailed = false;
} catch (error) {
importFailed = true; // Expected
}
expect(importFailed).toBe(true);
});Integration Test - Event Flow:
it('should complete full order lifecycle', async () => {
await createProduct({ sku: 'TEST-001', stockQuantity: 100 });
const order = await placeOrder({ sku: 'TEST-001', quantity: 5 });
await waitForEvents(500);
const updatedOrder = await getOrder(order.orderId);
expect(['CONFIRMED', 'PAID', 'SHIPPED']).toContain(updatedOrder.status);
});See docs/TESTING.md for complete testing guide.
| Layer | Technology |
|---|---|
| Runtime | Node.js 18+ |
| Language | TypeScript (strict mode) |
| Framework | Next.js 15 (App Router) |
| Monorepo | Nx |
| Database | SQLite + TypeORM |
| Event Bus | In-memory pub/sub |
| Testing | Jest |
- ARCHITECTURE.md - Design principles and patterns
- API.md - Complete endpoint reference with examples
- EVENT_FLOWS.md - Event choreography and diagrams
- TESTING.md - Testing strategy and examples
This project demonstrates:
- Modular Monolith - Clear boundaries without microservices complexity
- Domain-Driven Design - Rich models, value objects, aggregates
- Event-Driven Architecture - Loose coupling via domain events
- Vertical Slice Architecture - Features organized by capability
- Test-Driven Development - 310+ tests with architecture enforcement
- Type Safety - Strict TypeScript with business types
- Clean Architecture - Separation of concerns across layers
- ❌ Production-ready e-commerce platform
- ❌ Microservices example
- ❌ Real payment processing
- ❌ Authentication/authorization system
- ❌ Horizontally scalable architecture
This is a research project focused on DDD patterns and modular monolith architecture.
If building for production:
- External message broker (RabbitMQ/Kafka)
- PostgreSQL for production database
- Redis for caching
- Authentication and authorization
- Real payment gateway integration
- Distributed tracing
- API rate limiting
- Circuit breakers
- Monitoring and alerting
- CI/CD pipeline
We welcome contributions! See CONTRIBUTING.md for guidelines on how to contribute to this research project.
For questions or to discuss ideas, please open an issue or contact Maurício Carvalho at mauriciocarvalho@ita.br.
Built with ❤️ to demonstrate modular monolith architecture with DDD for cloud-native applications
This project is part of academic research at ITA investigating Domain-Driven Design (DDD) patterns and modular monolith architectures for cloud-native applications. This codebase serves as a practical demonstration of the guidelines proposed in the research paper.
Paper: Towards a Progressive Scalability for Modular Monolith Applications
Conference: WEBIST 2025 - 20th International Conference on Web Information Systems and Technologies
Publisher: SCITEPRESS | Year: 2025 | Paper ID: 137868
Author:
- Maurício Carvalho - Department of Computing Science, ITA, São José dos Campos, Brazil
Research Advisors:
- Prof. Juliana de Melo Bezerra, PhD - Department of Computing Science, ITA
- Prof. Karla Donato Fook, PhD - Department of Computing Science, ITA
Cloud-native software startups face intense pressure from limited resources, high uncertainty, and the need for rapid validation. Early architectural decisions have lasting effects on scalability, maintainability, and adaptability. Although microservices are often favored for their modularity, they introduce significant operational overhead and require organizational maturity that many startups lack. Traditional monoliths offer simplicity but tend to evolve into rigid, tightly coupled systems. When designed with disciplined modularity, modular monoliths can offer internal boundaries that support sustainable growth while avoiding the fragmentation and complexity of premature microservices adoption. This paper proposes guidelines for designing scalable modular monoliths, maintaining architectural flexibility, and reducing complexity, thereby supporting long-term evolution under typical startup constraints.
Keywords: Software Engineering, Software Architecture, Cloud Computing, Modular Monolith, Microservices.
@inproceedings{carvalho2025modular,
title={Towards a Progressive Scalability for Modular Monolith Applications},
author={Carvalho, Mauricio and Bezerra, Juliana de Melo and Fook, Karla Donato},
booktitle={Proceedings of the 20th International Conference on Web Information Systems and Technologies (WEBIST 2025)},
year={2025},
publisher={SCITEPRESS - Science and Technology Publications},
pages={137868},
url={https://www.scitepress.org/Papers/2025/137868/137868.pdf}
}MIT
