Skip to main content

Clean Architecture & Domain-Driven Design

This project implements Clean Architecture with Domain-Driven Design (DDD) principles and the CQRS (Command Query Responsibility Segregation) pattern.

What is Clean Architecture?

Clean Architecture, introduced by Robert C. Martin (Uncle Bob), is a software design philosophy that emphasizes:

Independence

Business logic is independent of frameworks, UI, and databases

Testability

Business rules can be tested without external dependencies

Flexibility

UI and infrastructure can change without affecting business logic

Maintainability

Clear separation of concerns makes code easier to maintain

The Dependency Rule

The fundamental rule is that dependencies point inward:
Infrastructure → Application → Domain
     (outer)        (middle)      (core)
  • Domain knows nothing about Application or Infrastructure
  • Application knows about Domain, but not Infrastructure
  • Infrastructure knows about both Application and Domain
No code in an inner layer can reference anything in an outer layer. This includes functions, classes, variables, or any other named software entity.

Domain-Driven Design (DDD)

DDD focuses on modeling software based on the business domain. Key concepts:

Entities and Value Objects

In our usuario module, Usuario is an entity:
src/app/usuario/domain/models/usuario.ts
export interface Usuario {
    id: number,
    usuario: string,
    contrasena: string
}
Characteristics:
  • Has a unique identifier (id)
  • Represents a business concept (user)
  • Lives in the Domain layer
  • No framework dependencies

Repository Pattern

Repositories provide an abstraction for data access:
src/app/usuario/domain/repositories/usuario.repository.ts
import { Observable } from "rxjs";
import { Usuario } from "../models/usuario";

export abstract class UsuarioRepository {
    abstract loginUsuario(usuario: string, contrasena: string): Observable<Usuario>;
    abstract registrarUsuario(usuario: string, contrasena: string): Observable<Usuario>;
}
Key Points:
  • Abstract class defines the interface (contract)
  • Located in Domain layer
  • Implementation is in Infrastructure layer
  • This is Dependency Inversion in action
In TypeScript/Angular, abstract classes can be used as injection tokens for dependency injection, while interfaces cannot (they disappear after compilation). This allows:
constructor(private usuarioRepository: UsuarioRepository) {}
Angular can inject the concrete implementation at runtime.

Domain Services vs Application Services

  • Domain Services: Contain business logic that doesn’t belong to a single entity
  • Application Services: Orchestrate use cases using domain objects and repositories
In this project, handlers are application services.

CQRS Pattern

Command Query Responsibility Segregation separates read and write operations:

Commands

Write Operations - Modify state
  • Create
  • Update
  • Delete

Queries

Read Operations - Retrieve data
  • Fetch
  • List
  • Search

Commands: Write Operations

Commands represent intent to change state:
src/app/usuario/application/commands/register-user.command.ts
export class RegisterUserCommand {
    constructor(
        readonly usuario: string,
        readonly contrasena: string
    ) {}
}
Characteristics:
  • Immutable (readonly properties)
  • Descriptive name (verb-based: Register, Create, Update)
  • Contains all data needed for the operation
  • No business logic

Command Handlers

Handlers execute commands:
src/app/usuario/application/usecases/commandHandlers/register-user.handler.ts
import { Injectable } from "@angular/core";
import { UsuarioRepository } from "../../../domain/repositories/usuario.repository";
import { RegisterUserCommand } from "../../commands/register-user.command";
import { Observable } from "rxjs";
import { Usuario } from "../../../domain/models/usuario";

@Injectable()
export class RegisterUserHandler {
    constructor(private usuarioRepository: UsuarioRepository) {}

    handle(command: RegisterUserCommand): Observable<Usuario> {
        return this.usuarioRepository.registrarUsuario(
            command.usuario,
            command.contrasena
        );
    }
}
Key Points:
  • Depends on UsuarioRepository interface (from Domain)
  • Injectable for dependency injection
  • Single responsibility: handle one command
  • Returns Observable for async operations

Queries: Read Operations

Queries represent requests for data:
src/app/usuario/application/queries/login-user.query.ts
export class LoginUserQuery {
    constructor(
        readonly usuario: string,
        readonly contrasena: string
    ) {}
}
In this case, “login” is a query because it reads user data to verify credentials. It doesn’t modify the user entity.

Query Handlers

Handlers execute queries:
src/app/usuario/application/usecases/queryHandlers/login-user.handler.ts
import { Observable } from "rxjs";
import { UsuarioRepository } from "../../../domain/repositories/usuario.repository";
import { LoginUserQuery } from "../../queries/login-user.query";
import { Usuario } from "../../../domain/models/usuario";
import { Injectable } from "@angular/core";

@Injectable()
export class LoginUserHandler {
    constructor(private usuarioRepository: UsuarioRepository) {}

    handle(query: LoginUserQuery): Observable<Usuario> {
        return this.usuarioRepository.loginUsuario(
            query.usuario,
            query.contrasena
        );
    }
}
Pattern:
  • Same structure as command handlers
  • Separated by intent (query vs command)
  • Allows different optimizations for reads vs writes

Dependency Inversion Principle

The Dependency Inversion Principle (DIP) states:
High-level modules should not depend on low-level modules. Both should depend on abstractions.

How It Works in This Project

1

Define Interface in Domain

The abstract repository lives in the Domain layer:
// domain/repositories/usuario.repository.ts
export abstract class UsuarioRepository {
    abstract registrarUsuario(...): Observable<Usuario>;
}
2

Implement in Infrastructure

The concrete implementation lives in Infrastructure:
// infrastructure/services/auth-service.ts
export class AuthService implements UsuarioRepository {
    constructor(private httpClient: HttpClient) {}
    
    registrarUsuario(usuario: string, contrasena: string): Observable<Usuario> {
        return this.httpClient.post<Usuario>(
            `${this.BASE_URL}registro`,
            { usuario, contrasena }
        );
    }
}
3

Use in Application

Handlers depend on the abstraction, not the implementation:
// application/usecases/commandHandlers/register-user.handler.ts
export class RegisterUserHandler {
    constructor(private usuarioRepository: UsuarioRepository) {}
    
    handle(command: RegisterUserCommand): Observable<Usuario> {
        return this.usuarioRepository.registrarUsuario(...);
    }
}

Benefits

Easy Testing

// Test with a mock repository
const mockRepo = {
  registrarUsuario: () => of(mockUser)
};
const handler = new RegisterUserHandler(mockRepo);

Swappable Implementations

// Switch from HTTP to LocalStorage
// without changing handlers
class LocalStorageUserRepository 
  implements UsuarioRepository { ... }

Real-World Flow Example

Let’s trace a complete user registration flow:
1

User Action

User fills registration form and clicks submit
// infrastructure/ui/pages/register-page/register-page.ts
registrar(usuario: string, contrasena: string) {
    this.authService.registrarUsuario(usuario, contrasena)
        .pipe(
            tap(usuario => {
                console.log('usuario', usuario);
                usuario ? alert('usuario registrado correctamente') : alert(usuario);
            }),
            catchError(err => {
                alert(`Error al registrar: ${err.error}`);
                return throwError(() => err);
            })
        ).subscribe();
}
2

Service Layer

AuthService (Infrastructure) implements the repository interface:
// infrastructure/services/auth-service.ts
@Injectable({ providedIn: 'root' })
export class AuthService implements UsuarioRepository {
    private readonly BASE_URL = 'http://localhost:8080/'
    
    registrarUsuario(usuario: string, contrasena: string): Observable<Usuario> {
        const body = { usuario, contrasena };
        return this.httpClient.post<Usuario>(
            `${this.BASE_URL}registro`,
            body,
            { headers: { 'Content-Type': 'application/json' } }
        );
    }
}
3

Handler (Alternative Flow)

If using handlers, the component would call a handler instead:
// Create command
const command = new RegisterUserCommand(usuario, contrasena);

// Execute via handler
this.registerUserHandler.handle(command).subscribe(...);
4

Domain Contract

All layers respect the domain contract:
// domain/repositories/usuario.repository.ts
export abstract class UsuarioRepository {
    abstract registrarUsuario(
        usuario: string,
        contrasena: string
    ): Observable<Usuario>;
}

Best Practices

Naming Conventions

  • Commands: {Verb}{Entity}Command (e.g., RegisterUserCommand, UpdateProfileCommand)
  • Queries: {Verb}{Entity}Query (e.g., GetUserQuery, ListProductsQuery)
  • Handlers: {Verb}{Entity}Handler (e.g., RegisterUserHandler, LoginUserHandler)
  • Repositories: {Entity}Repository (e.g., UsuarioRepository, ProductRepository)

When to Use Commands vs Queries

  • Create new entities
  • Update existing entities
  • Delete entities
  • Any operation that changes system state
  • May have side effects (send email, update cache, etc.)
  • Fetch single entity
  • List multiple entities
  • Search/filter entities
  • Login (reads user data for verification)
  • No side effects on domain state

Layer Responsibilities

LayerResponsibilitiesWhat NOT to Include
DomainEntities, value objects, repository interfaces, business rulesFramework code, HTTP calls, UI logic
ApplicationCommands, queries, handlers, use case orchestrationHTTP implementation, Angular components
InfrastructureUI components, HTTP services, routing, external APIsBusiness rules, domain logic

Benefits in Practice

Testability Example

// Easy to test handlers with mocks
describe('RegisterUserHandler', () => {
  it('should register user', (done) => {
    const mockRepo = {
      registrarUsuario: jasmine.createSpy().and.returnValue(
        of({ id: 1, usuario: 'test', contrasena: 'pass' })
      )
    };
    
    const handler = new RegisterUserHandler(mockRepo as any);
    const command = new RegisterUserCommand('test', 'pass');
    
    handler.handle(command).subscribe(result => {
      expect(result.usuario).toBe('test');
      expect(mockRepo.registrarUsuario).toHaveBeenCalled();
      done();
    });
  });
});

Maintainability Example

Need to switch from REST API to GraphQL?
// Just create a new service implementing the same interface
@Injectable()
export class GraphQLAuthService implements UsuarioRepository {
    constructor(private apollo: Apollo) {}
    
    registrarUsuario(usuario: string, contrasena: string): Observable<Usuario> {
        return this.apollo.mutate({
            mutation: REGISTER_USER_MUTATION,
            variables: { usuario, contrasena }
        }).pipe(map(result => result.data.registerUser));
    }
}

// Update provider - no other code changes needed!

Next Steps

Folder Structure

Explore the complete directory structure and conventions

Architecture Overview

Review the high-level architecture diagram

Build docs developers (and LLMs) love