← Back to blog

Why I chose Domain-Driven Design for ArtFolio

DDDNestJSArchitecture

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 sits at the center and depends on nothing. Each outer layer depends on the inner one, never the other way around.

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.

The cache follows the same principle. It goes through NestJS’s cache-manager abstraction: the store is in-memory today, but swapping it for Redis would only take a configuration change, without touching a single use case. The business logic has no idea where the data lives.

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.

The same path for every request, even when the operation is trivial.

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.

A single transaction groups the four writes. On failure, everything is rolled back and uploaded files are cleaned up.

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.