Structuring Your Project with Bounded Contexts in NestJS

codanykscodanyks
3 min read

“Don’t share a model between unrelated concepts—split your domain. That’s how you scale both the code and the team.” – DDD wisdom


What is a Bounded Context?

In DDD, a Bounded Context is a logical boundary around a part of your system that has its own models, language, and rules.

Each context:

  • Defines its own domain terms

  • Is autonomous in behavior

  • Interacts with others through well-defined interfaces

Think of it as a “domain module” that owns its logic, instead of dumping everything into shared services or models.


Why You Need It

Without Bounded Contexts, things get messy:

  • Business terms collide (e.g., "User" means 3 different things)

  • Codebases become tightly coupled

  • Changes in one feature break others

Bounded Contexts allow teams and systems to scale independently.


How NestJS Makes This Easy

NestJS has a modular architecture baked in. You already use Modules — we’ll now align them with Bounded Contexts.

A typical project structure:

src/
  ├── user/
  │   ├── user.module.ts
  │   ├── user.controller.ts
  │   ├── user.service.ts
  │   ├── user.entity.ts
  │   ├── user.value-object.ts
  │   └── use-cases/
  │       └── create-user.use-case.ts
  ├── auth/
  ├── payment/
  └── shared/

Each folder under src/ is a Bounded Context and owns its:

  • Domain logic (Entities, Value Objects)

  • Application logic (Use cases)

  • API (Controllers)

In NestJS, you don’t need a separate domain/ folder — your feature module already encapsulates domain logic effectively. Keep your module clean, and you’re good.


Naming and Language Matter

In DDD, the language inside each context is sacred.

For example:

  • In payments, a “Transaction” might be a domain Entity

  • In analytics, “Transaction” might mean a record of user behavior

Keep models context-specific. Do NOT reuse entities across domains. If they must talk, use DTOs, events, or interfaces.


Shared Modules ≠ Shared Logic

Avoid putting core logic in shared/:

  • Use shared/ only for cross-cutting concerns (e.g., logger, config)

  • NEVER put domain logic here

If two contexts need the same logic — extract it to a library, or rethink if they should even share it.


Example: User + Payment Contexts

Here’s how your Bounded Contexts might look:

src/
  ├── user/
  │   ├── user.module.ts
  │   ├── user.controller.ts
  │   ├── user.service.ts
  │   ├── user.entity.ts
  │   ├── user.value-object.ts
  │   └── use-cases/
  ├── payment/
  │   ├── payment.module.ts
  │   ├── payment.controller.ts
  │   ├── payment.service.ts
  │   ├── transaction.entity.ts
  │   └── use-cases/

Each context is independent and focused. They don’t import each other’s models or services directly.


Communication Between Contexts

Contexts often need to talk. Here’s how to keep it clean:

  • REST or GraphQL: Expose endpoints for other services to consume

  • Domain Events: Publish events when something changes (e.g., UserCreated)

  • Interfaces/Ports: Use abstraction (Dependency Injection) to decouple logic

NestJS supports all of this out of the box — especially with its CQRS and EventEmitter modules.


Takeaways

  • Bounded Contexts are the foundation of DDD architecture

  • In NestJS, use Modules to define and isolate each context

  • Keep domain language pure and consistent inside each context

  • Avoid leaking models or services across boundaries

  • You don’t need a domain/ folder if your module stays clean and modular


Coming Up Next

In the next post, we’ll dig into Entities, Value Objects, and Repositories — the core building blocks of your domain model.


What Do You Think?

Got questions on structuring modules or managing context boundaries in real-world apps?
X @codanyks — or share your current architecture, and let’s improve it together!

0
Subscribe to my newsletter

Read articles from codanyks directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

codanyks
codanyks