Entities, Value Objects, and Repositories in NestJS DDD

codanykscodanyks
3 min read

In 2025, building software means more than just spinning up controllers and services. We care about expressiveness, maintainability, and code that maps to the real world.

“It’s no longer 1937”

Why These Three Matter

In DDD, Entities, Value Objects, and Repositories are foundational:

  • Entities have identity — they live and evolve

  • Value Objects have meaning, not identity — they're immutable

  • Repositories abstract persistence — so your domain isn’t a slave to your database

These are your modeling tools — not just patterns, but how you think about code.


Entities in NestJS

Let’s say you’re building a User context. Here’s a simplified example:

// user.entity.ts
export class User {
  constructor(
    public readonly id: string,
    public name: string,
    public email: string
  ) {}

  rename(newName: string) {
    this.name = newName;
  }
}

Key points:

  • Identity (id) defines the entity

  • Behavior lives with the data (rename)

  • No ORM decorators here — this is pure domain

Keep your Entity clean. Leave persistence concerns elsewhere.


Value Objects

Let’s give the email some domain rules.

// value-objects/email.vo.ts
export class Email {
  constructor(private readonly value: string) {
    if (!/^[^@]+@[^@]+\.[^@]+$/.test(value)) {
      throw new Error('Invalid email format');
    }
  }

  getValue(): string {
    return this.value;
  }
}

Use it inside your User entity:

import { Email } from './value-objects/email.vo';

export class User {
  constructor(
    public readonly id: string,
    public name: string,
    public email: Email
  ) {}
}

Benefits:

  • Strong typing

  • Encapsulated rules

  • Cleaner entities


Repositories

Repositories abstract how data is fetched/saved.

// user.repository.ts
export interface UserRepository {
  findById(id: string): Promise<User | null>;
  save(user: User): Promise<void>;
}

Implementation using Prisma, TypeORM, etc., can live in infra/ or alongside the interface with dependency injection.

// user-prisma.repository.ts
@Injectable()
export class PrismaUserRepository implements UserRepository {
  constructor(private readonly prisma: PrismaService) {}

  async findById(id: string): Promise<User | null> {
    const record = await this.prisma.user.findUnique({ where: { id } });
    return record ? new User(record.id, record.name, new Email(record.email)) : null;
  }

  async save(user: User): Promise<void> {
    await this.prisma.user.upsert({
      where: { id: user.id },
      update: { name: user.name, email: user.email.getValue() },
      create: { id: user.id, name: user.name, email: user.email.getValue() },
    });
  }
}

Notice: The repository deals with persistence. The entity doesn’t care.


Folder Structure (NestJS-style, Clean)

src/
  └── user/
      ├── user.module.ts
      ├── user.controller.ts
      ├── user.service.ts
      ├── user.entity.ts
      ├── value-objects/
      │   └── email.vo.ts
      ├── user.repository.ts
      ├── prisma-user.repository.ts
      └── use-cases/
          └── create-user.use-case.ts

All domain logic stays within the module. No separate domain/ folder — it’s 2025, we’ve got better patterns now.


Quick Recap

Entity = Identity + Behavior
Value Object = Immutable + Rules
Repository = Persistence abstraction

And remember:

  • Your ORM is a tool, not your architecture

  • Business rules go in domain models, not services

  • Design for intention, not just implementation


Up Next

In the next article, we’ll look at Use Cases & Application Services — how your app orchestrates domain logic without leaking responsibilities.


What’s Your Take?

Are you using entities and value objects in your NestJS project? Or still juggling DTOs everywhere?
Tag @codanyks and show us your structure!

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