Entities, Value Objects, and Repositories in NestJS DDD


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 entityBehavior 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!
Subscribe to my newsletter
Read articles from codanyks directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
