Skip to main content

Folder Structure

Karma Ecommerce follows a feature-based modular architecture where each feature is organized into Domain, Application, and Infrastructure layers.

Complete Directory Tree

src/
├── app/
│   ├── app.config.ts           # Application configuration
│   ├── app.routes.ts           # Routing configuration
│   ├── app.ts                  # Root application component
│   ├── app.spec.ts             # Root component tests
│   └── usuario/                # User module (feature)
│       ├── domain/             # Domain layer
│       │   ├── models/         # Domain entities
│       │   │   └── usuario.ts  # User entity interface
│       │   └── repositories/   # Repository interfaces
│       │       └── usuario.repository.ts
│       ├── application/        # Application layer
│       │   ├── commands/       # Write operations
│       │   │   └── register-user.command.ts
│       │   ├── queries/        # Read operations
│       │   │   └── login-user.query.ts
│       │   └── usecases/       # Use case handlers
│       │       ├── commandHandlers/
│       │       │   └── register-user.handler.ts
│       │       └── queryHandlers/
│       │           └── login-user.handler.ts
│       └── infrastructure/     # Infrastructure layer
│           ├── services/        # External services
│           │   ├── auth-service.ts
│           │   └── auth-service.spec.ts
│           └── ui/             # User interface
│               └── pages/      # Page components
│                   ├── login-page/
│                   │   ├── login-page.ts
│                   │   ├── login-page.html
│                   │   ├── login-page.css
│                   │   └── login-page.spec.ts
│                   └── register-page/
│                       ├── register-page.ts
│                       ├── register-page.html
│                       ├── register-page.css
│                       └── register-page.spec.ts
└── main.ts                     # Application entry point

Root Level Structure

Application Files

main.ts

Application entry point that bootstraps the Angular app

app.config.ts

Application-wide configuration (providers, routes, HTTP client)

app.routes.ts

Top-level routing configuration

app.ts

Root component of the application

Configuration Example

src/app/app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
    provideHttpClient()  // Global HTTP client
  ]
};

Feature Module Structure

Each feature (e.g., usuario) follows the same three-layer structure.

The Usuario Module

The usuario module handles user authentication and registration. It demonstrates the complete clean architecture pattern:
usuario/
├── domain/              # Pure business logic
├── application/         # Use cases and orchestration
└── infrastructure/      # External concerns (UI, HTTP)

Domain Layer

The innermost layer with no dependencies on frameworks or outer layers.

Structure

domain/
├── models/              # Domain entities and value objects
│   └── usuario.ts       # User entity
└── repositories/        # Repository interfaces (contracts)
    └── usuario.repository.ts

Models

Domain entities represent core business concepts:
src/app/usuario/domain/models/usuario.ts
export interface Usuario {
    id: number,
    usuario: string,
    contrasena: string
}
Characteristics:
  • Pure TypeScript interfaces or classes
  • No framework imports
  • Represent business entities
  • Contain validation rules (when applicable)

Repository Interfaces

Define contracts 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>;
}
Repositories are abstract classes (not interfaces) to work with Angular’s dependency injection system.

Application Layer

Orchestrates use cases using domain objects and repository interfaces.

Structure

application/
├── commands/            # Write operations (mutations)
│   └── register-user.command.ts
├── queries/             # Read operations
│   └── login-user.query.ts
└── usecases/            # Handlers that execute commands/queries
    ├── commandHandlers/
    │   └── register-user.handler.ts
    └── queryHandlers/
        └── login-user.handler.ts

Commands (Write Operations)

Represent intent to change state:
src/app/usuario/application/commands/register-user.command.ts
export class RegisterUserCommand {
    constructor(
        readonly usuario: string,
        readonly contrasena: string
    ) {}
}
Naming Convention: {Verb}{Entity}Command
  • RegisterUserCommand
  • UpdateProfileCommand
  • DeleteAccountCommand

Queries (Read Operations)

Represent requests for data:
src/app/usuario/application/queries/login-user.query.ts
export class LoginUserQuery {
    constructor(
        readonly usuario: string,
        readonly contrasena: string
    ) {}
}
Naming Convention: {Verb}{Entity}Query
  • LoginUserQuery
  • GetUserProfileQuery
  • ListUsersQuery

Command Handlers

Execute write operations:
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:
  • Injectable for dependency injection
  • Depends on repository interface (not implementation)
  • Single responsibility: one handler per command
  • Returns Observable for async operations

Query Handlers

Execute read operations:
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
        );
    }
}

Infrastructure Layer

Handles all external dependencies and framework-specific code.

Structure

infrastructure/
├── services/            # Concrete repository implementations
│   ├── auth-service.ts
│   └── auth-service.spec.ts
└── ui/                  # User interface components
    └── pages/           # Page-level components
        ├── login-page/
        │   ├── login-page.ts
        │   ├── login-page.html
        │   ├── login-page.css
        │   └── login-page.spec.ts
        └── register-page/
            ├── register-page.ts
            ├── register-page.html
            ├── register-page.css
            └── register-page.spec.ts

Services

Implement repository interfaces with concrete technology:
src/app/usuario/infrastructure/services/auth-service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Usuario } from '../../domain/models/usuario';
import { UsuarioRepository } from '../../domain/repositories/usuario.repository';

@Injectable({
  providedIn: 'root',
})
export class AuthService implements UsuarioRepository {
  private readonly BASE_URL = 'http://localhost:8080/'

  constructor(private httpClient: HttpClient) {}

  loginUsuario(usuario: string, contrasena: string): Observable<Usuario> {
    return this.httpClient.get<Usuario>(
      this.BASE_URL + 'login' + '/' + usuario + '/' + contrasena
    );
  }

  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' } }
    );
  }
}
Key Points:
  • Implements UsuarioRepository interface
  • Uses Angular’s HttpClient for HTTP calls
  • Provided in root for singleton pattern
  • Can be easily swapped with different implementation

UI Components

Angular components for presentation:
src/app/usuario/infrastructure/ui/pages/register-page/register-page.ts
import { Component } from '@angular/core';
import { AuthService } from '../../../services/auth-service';
import { catchError, tap, throwError } from 'rxjs';

@Component({
  selector: 'app-register-page',
  imports: [],
  templateUrl: './register-page.html',
  styleUrl: './register-page.css',
})
export class RegisterPage {
  constructor(private authService: AuthService) {}

  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 => {
          const mensaje = err.error ? err.error : 'Error desconocido';
          alert(`Error al registrar: ${mensaje}`);
          return throwError(() => err);
        })
      )
      .subscribe();
  }
}
Component Structure:
  • Each page has 4 files: .ts, .html, .css, .spec.ts
  • Components are in ui/pages/ directory
  • Use services (not handlers directly) for simplicity
  • Handle UI concerns (loading states, errors, validation)

Naming Conventions

Files

TypePatternExample
Command{verb}-{entity}.command.tsregister-user.command.ts
Query{verb}-{entity}.query.tslogin-user.query.ts
Handler{verb}-{entity}.handler.tsregister-user.handler.ts
Repository{entity}.repository.tsusuario.repository.ts
Model{entity}.tsusuario.ts
Service{name}-service.tsauth-service.ts
Component{name}-page.tsregister-page.ts

Classes

TypePatternExample
Command{Verb}{Entity}CommandRegisterUserCommand
Query{Verb}{Entity}QueryLoginUserQuery
Handler{Verb}{Entity}HandlerRegisterUserHandler
Repository{Entity}RepositoryUsuarioRepository
Service{Name}ServiceAuthService
Component{Name}PageRegisterPage

Directory Responsibilities

Purpose: Define business entities and value objectsContains:
  • Interfaces or classes for entities
  • Value objects
  • Domain types
Rules:
  • No framework imports
  • Pure TypeScript
  • No business logic (entities are data structures)
Purpose: Define contracts for data accessContains:
  • Abstract classes or interfaces
  • Method signatures for data operations
Rules:
  • Abstract classes (for Angular DI)
  • No implementation details
  • Return types use domain models
Purpose: Represent write operationsContains:
  • Command classes
  • Immutable data structures
Rules:
  • Readonly properties
  • No business logic
  • Verb-based naming
Purpose: Represent read operationsContains:
  • Query classes
  • Search/filter parameters
Rules:
  • Readonly properties
  • No business logic
  • Verb-based naming
Purpose: Execute commandsContains:
  • Handler classes
  • Command execution logic
Rules:
  • One handler per command
  • Use repository interfaces
  • Injectable services
Purpose: Execute queriesContains:
  • Handler classes
  • Query execution logic
Rules:
  • One handler per query
  • Use repository interfaces
  • Injectable services
Purpose: Implement repository interfacesContains:
  • Service classes
  • HTTP client code
  • External API integrations
Rules:
  • Implement repository interfaces
  • Use Angular HttpClient
  • Handle errors and transformations
Purpose: User interface componentsContains:
  • Angular components
  • Templates and styles
  • Component tests
Rules:
  • One component per feature/page
  • 4 files per component (ts, html, css, spec)
  • Use services for data access

Adding a New Feature

When adding a new feature (e.g., producto for products), follow this structure:
1

Create Feature Directory

mkdir -p src/app/producto
2

Create Layer Directories

mkdir -p src/app/producto/domain/models
mkdir -p src/app/producto/domain/repositories
mkdir -p src/app/producto/application/commands
mkdir -p src/app/producto/application/queries
mkdir -p src/app/producto/application/usecases/commandHandlers
mkdir -p src/app/producto/application/usecases/queryHandlers
mkdir -p src/app/producto/infrastructure/services
mkdir -p src/app/producto/infrastructure/ui/pages
3

Create Domain Layer

// producto.ts
export interface Producto {
  id: number;
  nombre: string;
  precio: number;
}

// producto.repository.ts
export abstract class ProductoRepository {
  abstract getProducto(id: number): Observable<Producto>;
  abstract createProducto(producto: Producto): Observable<Producto>;
}
4

Create Application Layer

// create-producto.command.ts
export class CreateProductoCommand {
  constructor(
    readonly nombre: string,
    readonly precio: number
  ) {}
}

// create-producto.handler.ts
export class CreateProductoHandler {
  constructor(private repo: ProductoRepository) {}
  handle(cmd: CreateProductoCommand): Observable<Producto> {
    return this.repo.createProducto({ nombre: cmd.nombre, precio: cmd.precio });
  }
}
5

Create Infrastructure Layer

// producto-service.ts
export class ProductoService implements ProductoRepository {
  constructor(private http: HttpClient) {}
  getProducto(id: number): Observable<Producto> {
    return this.http.get<Producto>(`/api/productos/${id}`);
  }
}

Testing Structure

Each layer has corresponding test files:
usuario/
├── infrastructure/
│   ├── services/
│   │   ├── auth-service.ts
│   │   └── auth-service.spec.ts        # Unit tests
│   └── ui/pages/
│       ├── login-page/
│       │   ├── login-page.ts
│       │   └── login-page.spec.ts      # Component tests
Testing Guidelines:
  • Unit tests for services and handlers
  • Component tests for UI
  • Mock repositories in handler tests
  • Use TestBed for Angular component tests

Best Practices

Keep Layers Separate

Never import from outer layers into inner layersInfrastructure → Application → Domain ✓Domain → Infrastructure ✗

Single Responsibility

Each file should have one clear purposeOne command per file ✓Multiple commands in one file ✗

Consistent Naming

Follow naming conventions strictlyregister-user.command.tsuserRegister.cmd.ts

Colocate Tests

Keep tests next to implementationauth-service.spec.ts next to auth-service.ts

Common Patterns

Import Paths

Relative imports respect layer boundaries:
// In handler (Application layer)
import { UsuarioRepository } from "../../../domain/repositories/usuario.repository";
import { RegisterUserCommand } from "../../commands/register-user.command";

// In service (Infrastructure layer)
import { Usuario } from "../../domain/models/usuario";
import { UsuarioRepository } from "../../domain/repositories/usuario.repository";

Barrel Exports (Optional)

Create index.ts files for cleaner imports:
src/app/usuario/domain/index.ts
export * from './models/usuario';
export * from './repositories/usuario.repository';
Then import:
import { Usuario, UsuarioRepository } from '../../../domain';

Next Steps

Clean Architecture

Understand the architectural principles

Architecture Overview

See the high-level architecture diagram

Build docs developers (and LLMs) love