← Back to blog

Why I chose Domain-Driven Design for ArtFolio

DDD NestJS Architecture

The context

When I started ArtFolio as my final training project, I had a choice between a simple architecture (controllers + services + repositories in one module) and something more structured. I chose Domain-Driven Design. Not for complexity's sake, but because the project had real business rules: user management with different roles (artist, amateur, moderator), file uploads, GDPR requests, and a granular permissions system.

What DDD changed in practice

The domain depends on nothing

The most important decision was keeping the domain layer completely independent from the framework. My entities, value objects, and repository interfaces are pure TypeScript. No NestJS decorators, no TypeORM references. When I test a business rule, I don't need a database or the NestJS DI container.

In practice, this means UserId is a value object that validates UUID format at construction. If someone tries to pass a PostId where a UserId is expected, the TypeScript compiler rejects it. That's safety at two levels: compile-time and runtime.

Use cases as units of work

Each business operation is an isolated use case: CreateArtistUseCase, GetAllPostsUseCase, HandlePersonalDataRequestUseCase. Each use case does exactly one thing. It receives its dependencies through injection (via interfaces, never concrete implementations) and orchestrates the business logic.

The advantage: when a test fails, I know exactly where to look. When a new developer joins the project, they can read a use case and understand a complete business flow without navigating across 10 files.

Infrastructure is replaceable

Repositories implement interfaces defined in the domain. If I wanted to swap PostgreSQL for MongoDB, I would only need to write new repository implementations. The domain and application layers wouldn't change.

This isn't a theoretical scenario. During development, I changed the caching strategy (from a simple in-memory object to Redis) without touching a single use case. The cache interface was the same, only the implementation changed.

The pitfalls I encountered

Too many layers for simple operations

For a simple GET that returns a list, the path Controller → Use Case → Repository → Database can feel excessive. I learned to accept that some use cases are trivial, and that's fine. Consistency of structure matters more than optimizing every file.

Mapping between layers

Transforming a DTO into a domain entity, then into a TypeORM entity, then into a response DTO is repetitive code. I used class-transformer and well-typed DTOs to reduce boilerplate, but it's still there. That's the cost of layer separation.

Cross-repository transactions

Creating an artist involves saving a user, an asset (profile picture), an initial post, and categories. All in one transaction. Pure DDD would say to use domain events, but in a NestJS backend of this size, I went with a DataSource.transaction() with a scoped EntityManager. If something fails, everything rolls back, and uploaded files get deleted. Pragmatic rather than dogmatic.

What I would do again

Yes, I would make the same choice. DDD is not an architecture to impress. It's a way to structure code so that business decisions are visible, testable, and isolated from infrastructure. For a project with real business invariants (roles, permissions, GDPR), it's the right tool.

For a simple CRUD with no business logic? A standard NestJS module is enough. Architecture should serve the problem, not the other way around.