This episode covers architecture and design patterns in NestJS: modular architecture and feature modules, domain-driven design and hexagonal architecture, event-driven architecture with CQRS and event sourcing, plus shared modules and inter-module boundaries.

As your application and team grow, how you structure your code determines long-term success. Episode 18 covers the architecture and design patterns that keep a codebase healthy: modular architecture, DDD, hexagonal architecture, and event-driven architecture.
You'll learn the patterns used by real-world enterprise-scale NestJS projects.
A feature module groups code by feature domain — the controller, service, entity, and DTO belonging to the same feature live in one module.
src/
modules/
users/
users.module.ts
users.controller.ts
users.service.ts
user.entity.ts
orders/
orders.module.ts
orders.controller.ts
orders.service.ts
order.entity.tsThis structure keeps the code easy to navigate and lets several developers work on different features without conflicts.
Each module has one responsibility. If a module grows too large, split it into submodules. This pattern keeps every part of the application focused and easy to test.
DDD focuses on modeling the business domain. Its key concepts are bounded context — the boundary where one domain model applies — and ubiquitous language — the same terms used by developers and domain experts.
import { Entity } from "typeorm";
@Entity()
export class Order {
id: number;
status: string;
items: OrderItem[];
addItem(item: OrderItem): void {
if (this.status === "completed") {
throw new Error("Order sudah selesai");
}
this.items.push(item);
}
}Domain entities hold business logic (invariants) — not just passive data.
DDD separates layers: domain (business logic), application (use cases), infrastructure (database, HTTP), and presentation (controllers). NestJS fits this pattern well because modules and providers map cleanly onto these layers.
Hexagonal architecture (ports and adapters) separates the application core from the outside world. The core doesn't depend on the database or HTTP — it communicates through ports, and adapters implement those ports.
export interface UserRepository {
findById(id: number): Promise<User>;
save(user: User): Promise<void>;
}A TypeORM adapter implements this port:
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { User } from "../domain/user.entity";
@Injectable()
export class TypeOrmUserRepository implements UserRepository {
constructor(
@InjectRepository(User)
private readonly repo: Repository<User>,
) {}
async findById(id: number): Promise<User> {
return this.repo.findOneBy({ id });
}
async save(user: User): Promise<void> {
await this.repo.save(user);
}
}The application core stays pure — swapping the database doesn't touch the business logic.
CQRS separates read operations (queries) from write operations (commands). NestJS has the @nestjs/cqrs module, installed via npm install @nestjs/cqrs:
npm install @nestjs/cqrsimport { CommandHandler, ICommand, ICommandHandler } from "@nestjs/cqrs";
export class CreateOrderCommand implements ICommand {
constructor(public readonly productId: number) {}
}
@CommandHandler(CreateOrderCommand)
export class CreateOrderHandler implements ICommandHandler<CreateOrderCommand> {
async execute(command: CreateOrderCommand): Promise<void> {
// logika membuat order
}
}Event sourcing stores state changes as a sequence of events, not just the final state. Each action produces an event that can be used for state reconstruction, audit trails, and replication between services. The combination of CQRS and event sourcing is the foundation of many large-scale event-driven systems.
A shared module exports providers for use by other modules. A global module (marked @Global()) is available throughout the application without being re-imported — suitable for cross-cutting concerns like logging and configuration.
Boundaries between modules keep dependencies clear: a module may only depend on what it explicitly imports. Avoid excessive global modules because they hide dependencies and make the module graph hard to understand.
Episode 18 maps out advanced architecture: feature modules, DDD, hexagonal architecture, event-driven with CQRS and event sourcing, and inter-module boundaries.
Key takeaways:
In the next episode 19 we'll discuss modern tooling and build automation — the Nest CLI, TypeScript compiler, and ts-node; build pipelines with npm and Docker; static analysis, linting, and formatting; plus reproducible builds and multi-environment setup.