Skip to content

Commit 9c2429c

Browse files
authored
Merge pull request #65 from raouf-b-dev/feature/ES-66-decouple-cross-module-dependencies-in-orders
Feature/es 66 decouple cross module dependencies in orders
2 parents 1e44098 + f6b4931 commit 9c2429c

File tree

18 files changed

+422
-71
lines changed

18 files changed

+422
-71
lines changed

ARCHITECTURE.md

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ This document provides a high-level overview of the **ecommerce-store-api** arch
55
## 📋 Table of Contents
66

77
- [System Context](#-system-context-c4-level-1)
8+
- [Strategic Domain-Driven Design](#-strategic-domain-driven-design)
89
- [High-Level Architecture](#-high-level-architecture)
910
- [Component Dependencies](#-component-dependencies-c4-level-2)
1011
- [Checkout Sequence Diagram](#-checkout-sequence-diagram-online-flow)
@@ -15,6 +16,58 @@ This document provides a high-level overview of the **ecommerce-store-api** arch
1516
- [Idempotency Logic](#-idempotency-logic)
1617
- [Notification System Architecture](#-notification-system-architecture)
1718

19+
## 🧠 Strategic Domain-Driven Design
20+
21+
We utilize **Strategic DDD** to define boundaries and relationships between different parts of the system.
22+
23+
### Subdomains
24+
25+
| Subdomain | Type | Description |
26+
| :---------------- | :-------------- | :----------------------------------------------------------------------------------------------------------------------- |
27+
| **Orders** | **Core Domain** | The heart of the business. Handles the complex lifecycle of customer orders, SAGA orchestration, and revenue generation. |
28+
| **Inventory** | Supporting | Manages stock levels. Essential but not the primary competitive advantage. |
29+
| **Products** | Supporting | Manages the catalog. Supports the core selling process. |
30+
| **Payments** | Generic | Handles transaction processing. Uses standard patterns (Stripe/PayPal) that can be bought/outsourced. |
31+
| **Auth** | Generic | Identity and Access Management. Standard JWT implementation. |
32+
| **Notifications** | Generic | Delivery mechanism for alerts. |
33+
34+
### Bounded Contexts & Context Mapping
35+
36+
Each Module acts as a **Bounded Context**. We use **Context Mapping** to define how they interact, strictly enforcing boundaries to prevent a "Big Ball of Mud".
37+
38+
```mermaid
39+
graph TD
40+
subgraph "Core Domain"
41+
Orders[Orders Context]
42+
end
43+
44+
subgraph "Supporting Subdomains"
45+
Inventory[Inventory Context]
46+
Products[Products Context]
47+
Carts[Carts Context]
48+
end
49+
50+
subgraph "Generic Subdomains"
51+
Customers[Customers Context]
52+
Payments[Payments Context]
53+
end
54+
55+
%% Relationships
56+
Orders -->|ACL / CustomerGateway| Customers
57+
Orders -->|ACL / CartGateway| Carts
58+
Carts -->|ACL / InventoryGateway| Inventory
59+
60+
classDef core fill:#ff9999,stroke:#333,stroke-width:2px;
61+
classDef support fill:#99ff99,stroke:#333,stroke-width:1px;
62+
classDef generic fill:#9999ff,stroke:#333,stroke-width:1px;
63+
64+
class Orders core;
65+
class Inventory,Products,Carts support;
66+
class Customers,Payments generic;
67+
```
68+
69+
> **Anti-Corruption Layer (ACL)**: The `Orders` context does **not** directly depend on the implementation of `Customers` or `Carts`. Instead, it defines its own **Ports** (Gateways), and we implement **Adapters** that translate external models into the Order domain's language. This protects the Core Domain from changes in upstream modules.
70+
1871
## 🌍 System Context (C4 Level 1)
1972

2073
A high-level view of how the E-commerce API fits into the existing landscape.
@@ -106,14 +159,14 @@ graph TD
106159
end
107160
108161
%% Orders Dependencies
109-
Orders -->|Reserves Stock| Inventory
110-
Orders -->|Processes Payment| Payments
111-
Orders -->|Validates User| Customers
112-
Orders -->|Retrieves Cart| Carts
113-
Orders -->|Triggers| Notifications
162+
Orders -->|ACL| Inventory
163+
Orders -->|ACL| Payments
164+
Orders -->|ACL| Customers
165+
Orders -->|ACL| Carts
166+
Orders -->|Event| Notifications
114167
115168
%% Carts Dependencies
116-
Carts -->|Checks Stock| Inventory
169+
Carts -->|ACL| Inventory
117170
Carts -->|Validates Item| Products
118171
119172
%% Auth Dependencies

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ I built this to prove (to myself and future employers) that I can handle complex
4949
### 🏗️ **Architecture & Design**
5050

5151
- **Domain-Driven Design (DDD)** with clear layer separation (Domain, Application, Infrastructure, Presentation)
52+
- **Strategic DDD** with explicit Subdomains (Core, Generic, Supporting) and Bounded Contexts
53+
- **Anti-Corruption Layer (ACL)** using Ports & Adapters to decouple modules
5254
- **Clean Architecture** principles ensuring the core logic is independent of frameworks and external tools
5355
- **Result Pattern** for consistent, type-safe error handling across the entire application
5456
- **Hexagonal Architecture (Ports & Adapters)** for easy swapping of infrastructure components (e.g., switching between Postgres and Redis repositories)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Result } from '../../../../core/domain/result';
2+
import { InfrastructureError } from '../../../../core/errors/infrastructure-error';
3+
import { CheckStockResponse } from '../../../inventory/presentation/dto/check-stock-response.dto';
4+
5+
export interface InventoryGateway {
6+
checkStock(
7+
productId: number,
8+
quantity: number,
9+
): Promise<Result<CheckStockResponse, InfrastructureError>>;
10+
}

src/modules/carts/application/usecases/add-cart-item/add-cart-item.usecase.spec.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ import { RepositoryError } from '../../../../../core/errors/repository.error';
99
import { AddCartItemDto } from '../../../presentation/dto/add-cart-item.dto';
1010
import { ProductRepository } from '../../../../products/domain/repositories/product-repository';
1111
import { IProduct } from '../../../../products/domain/interfaces/product.interface';
12+
import { InventoryGateway } from '../../ports/inventory.gateway';
1213

1314
describe('AddCartItemUseCase', () => {
1415
let usecase: AddCartItemUseCase;
1516
let mockCartRepository: MockCartRepository;
1617
let mockProductRepository: jest.Mocked<ProductRepository>;
17-
let mockCheckStockUseCase: any;
18+
let mockInventoryGateway: jest.Mocked<InventoryGateway>;
1819

1920
const mockProduct: IProduct = {
2021
id: 1,
@@ -35,14 +36,14 @@ describe('AddCartItemUseCase', () => {
3536
findAll: jest.fn(),
3637
deleteById: jest.fn(),
3738
} as any;
38-
mockCheckStockUseCase = {
39-
execute: jest.fn(),
39+
mockInventoryGateway = {
40+
checkStock: jest.fn(),
4041
};
4142

4243
usecase = new AddCartItemUseCase(
4344
mockCartRepository,
4445
mockProductRepository,
45-
mockCheckStockUseCase,
46+
mockInventoryGateway,
4647
);
4748
});
4849

@@ -70,7 +71,7 @@ describe('AddCartItemUseCase', () => {
7071
mockProductRepository.findById.mockResolvedValue(
7172
Result.success(mockProduct),
7273
);
73-
mockCheckStockUseCase.execute.mockResolvedValue(
74+
mockInventoryGateway.checkStock.mockResolvedValue(
7475
Result.success({
7576
isAvailable: true,
7677
availableQuantity: 10,
@@ -159,7 +160,7 @@ describe('AddCartItemUseCase', () => {
159160
mockProductRepository.findById.mockResolvedValue(
160161
Result.success(mockProduct),
161162
);
162-
mockCheckStockUseCase.execute.mockResolvedValue(
163+
mockInventoryGateway.checkStock.mockResolvedValue(
163164
Result.success({
164165
isAvailable: false,
165166
availableQuantity: 5,
@@ -171,10 +172,10 @@ describe('AddCartItemUseCase', () => {
171172
const result = await usecase.execute({ cartId, dto });
172173

173174
// Assert
174-
expect(mockCheckStockUseCase.execute).toHaveBeenCalledWith({
175-
productId: dto.productId,
176-
quantity: dto.quantity,
177-
});
175+
expect(mockInventoryGateway.checkStock).toHaveBeenCalledWith(
176+
dto.productId,
177+
dto.quantity,
178+
);
178179
ResultAssertionHelper.assertResultFailure(
179180
result,
180181
'Insufficient stock',

src/modules/carts/application/usecases/add-cart-item/add-cart-item.usecase.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Injectable } from '@nestjs/common';
1+
import { Inject, Injectable } from '@nestjs/common';
22
import { UseCase } from '../../../../../core/application/use-cases/base.usecase';
33
import { AddCartItemDto } from '../../../presentation/dto/add-cart-item.dto';
44
import { ICart } from '../../../domain/interfaces/cart.interface';
@@ -7,7 +7,8 @@ import { CartRepository } from '../../../domain/repositories/cart.repository';
77
import { ProductRepository } from '../../../../products/domain/repositories/product-repository';
88
import { isFailure, Result } from '../../../../../core/domain/result';
99
import { ErrorFactory } from '../../../../../core/errors/error.factory';
10-
import { CheckStockUseCase } from '../../../../inventory/application/check-stock/check-stock.usecase';
10+
import { InventoryGateway } from '../../ports/inventory.gateway';
11+
import { INVENTORY_GATEWAY } from '../../../carts.token';
1112

1213
@Injectable()
1314
export class AddCartItemUseCase extends UseCase<
@@ -18,7 +19,8 @@ export class AddCartItemUseCase extends UseCase<
1819
constructor(
1920
private readonly cartRepository: CartRepository,
2021
private readonly productRepository: ProductRepository,
21-
private readonly checkStockUseCase: CheckStockUseCase,
22+
@Inject(INVENTORY_GATEWAY)
23+
private readonly inventoryGateway: InventoryGateway,
2224
) {
2325
super();
2426
}
@@ -52,10 +54,10 @@ export class AddCartItemUseCase extends UseCase<
5254
}
5355

5456
// Check stock availability
55-
const stockCheckResult = await this.checkStockUseCase.execute({
56-
productId: dto.productId,
57-
quantity: dto.quantity,
58-
});
57+
const stockCheckResult = await this.inventoryGateway.checkStock(
58+
dto.productId,
59+
dto.quantity,
60+
);
5961

6062
if (isFailure(stockCheckResult)) {
6163
return ErrorFactory.UseCaseError(stockCheckResult.error.message);

src/modules/carts/carts.module.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@ import { UpdateCartItemController } from './presentation/controllers/update-cart
1111
import { CartEntity } from './infrastructure/orm/cart.schema';
1212
import { CartItemEntity } from './infrastructure/orm/cart-item.schema';
1313
import { RedisModule } from '../../core/infrastructure/redis/redis.module';
14-
import { POSTGRES_CART_REPOSITORY, REDIS_CART_REPOSITORY } from './carts.token';
14+
import {
15+
POSTGRES_CART_REPOSITORY,
16+
REDIS_CART_REPOSITORY,
17+
INVENTORY_GATEWAY,
18+
} from './carts.token';
1519
import { PostgresCartRepository } from './infrastructure/repositories/postgres-cart-repository/postgres.cart-repository';
1620
import { RedisCartRepository } from './infrastructure/repositories/redis-cart-repository/redis.cart-repository';
21+
import { ModuleInventoryGateway } from './infrastructure/adapters/module-inventory.gateway';
1722
import { CacheService } from '../../core/infrastructure/redis/cache/cache.service';
1823
import { CartRepository } from './domain/repositories/cart.repository';
1924
import { InventoryModule } from '../inventory/inventory.module';
@@ -58,6 +63,12 @@ import { ProductsModule } from '../products/products.module';
5863
inject: [CacheService, POSTGRES_CART_REPOSITORY],
5964
},
6065

66+
// Gateways
67+
{
68+
provide: INVENTORY_GATEWAY,
69+
useClass: ModuleInventoryGateway,
70+
},
71+
6172
// Default Repository Binding
6273
{
6374
provide: CartRepository,

src/modules/carts/carts.token.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export const POSTGRES_CART_REPOSITORY = Symbol('POSTGRES_CART_REPOSITORY');
22
export const REDIS_CART_REPOSITORY = Symbol('REDIS_CART_REPOSITORY');
3+
export const INVENTORY_GATEWAY = Symbol('INVENTORY_GATEWAY');
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { InventoryGateway } from '../../application/ports/inventory.gateway';
3+
import { CheckStockUseCase } from '../../../inventory/application/check-stock/check-stock.usecase';
4+
import { Result, isFailure } from '../../../../core/domain/result';
5+
import { InfrastructureError } from '../../../../core/errors/infrastructure-error';
6+
import { CheckStockResponse } from '../../../inventory/presentation/dto/check-stock-response.dto';
7+
import { ErrorFactory } from '../../../../core/errors/error.factory';
8+
9+
@Injectable()
10+
export class ModuleInventoryGateway implements InventoryGateway {
11+
constructor(private readonly checkStockUseCase: CheckStockUseCase) {}
12+
13+
async checkStock(
14+
productId: number,
15+
quantity: number,
16+
): Promise<Result<CheckStockResponse, InfrastructureError>> {
17+
const result = await this.checkStockUseCase.execute({
18+
productId,
19+
quantity,
20+
});
21+
22+
if (isFailure(result)) {
23+
return ErrorFactory.InfrastructureError(
24+
'Failed to check stock',
25+
result.error,
26+
);
27+
}
28+
29+
return Result.success(result.value);
30+
}
31+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Result } from '../../../../core/domain/result';
2+
import { ICart } from '../../../carts/domain/interfaces/cart.interface';
3+
import { InfrastructureError } from '../../../../core/errors/infrastructure-error';
4+
5+
export interface CartGateway {
6+
validateCart(cartId: number): Promise<Result<ICart, InfrastructureError>>;
7+
getCart(cartId: number): Promise<Result<ICart, InfrastructureError>>;
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Result } from '../../../../core/domain/result';
2+
import { ICustomer } from '../../../customers/domain/interfaces/customer.interface';
3+
import { InfrastructureError } from '../../../../core/errors/infrastructure-error';
4+
5+
export interface CustomerGateway {
6+
validateCustomer(
7+
userId: number,
8+
): Promise<Result<ICustomer, InfrastructureError>>;
9+
}

0 commit comments

Comments
 (0)