zaro

What is Clean Architecture in Angular?

Published in Angular Architecture 7 mins read

Clean Architecture in Angular is a software design philosophy that structures an application into distinct, concentric layers, ensuring a clear separation of concerns, improved testability, and enhanced maintainability and scalability. The clean architecture pattern separates the concerns of an application into distinct layers, promoting modularity, testability, and maintainability. By adhering to clean architecture principles, developers can achieve a highly decoupled and scalable Angular application.

Understanding Clean Architecture Principles

At its core, Clean Architecture aims to create systems that are independent of frameworks, databases, UI, and external agencies. It achieves this by defining a set of principles that dictate how code should be organized and dependencies should flow.

The Core Idea: Separation of Concerns

This principle states that each part of an application should have a single, well-defined responsibility. In Angular, this translates to designing components, services, and modules to focus on specific tasks, rather than bundling multiple responsibilities into one. This separation makes the codebase easier to understand, modify, and test.

The Dependency Rule

The most crucial principle of Clean Architecture is the Dependency Rule. It dictates that dependencies must always flow inward. Inner layers should never depend on outer layers. This means:

  • Entities (Core Business Rules) are the innermost layer and have no dependencies.
  • Use Cases (Application Business Rules) depend only on Entities.
  • Interface Adapters depend on Use Cases and Entities.
  • Frameworks & Drivers (UI, Database, etc.) are the outermost layer and depend on everything else.

This rule creates a system where changes in external frameworks or the UI do not ripple down to affect core business logic, making the application robust and adaptable.

Layers of Clean Architecture in Angular

While Angular itself doesn't enforce a specific architecture, Clean Architecture can be effectively mapped to its structure, leading to a highly organized and robust application. Here's a breakdown of the typical layers and how they translate to Angular:

Clean Architecture Layer Description Angular Mapping (Examples)
Entities (Domain) Contains the enterprise-wide business rules and core data structures. These are independent of any application, UI, or database. Angular Interfaces, Types, or simple Classes representing core business models (e.g., User, Product, Order). Often placed in domain/models.
Use Cases (Application) Encapsulates application-specific business rules. It orchestrates the flow of data to and from the Entities. These are specific to a single application. Angular Services that implement specific application functionalities (e.g., GetUsersUseCase, CreateOrderUseCase). Often in application/use-cases.
Interface Adapters This layer converts data from the format most convenient for the Use Cases and Entities, to the format most convenient for the external frameworks. Angular Services that act as repositories (e.g., UserRepository, ProductApiAdapter), View Models, or Presenters that prepare data for the UI.
Frameworks & Drivers The outermost layer, consisting of frameworks, databases, and UI. This layer typically contains all the "details" that change frequently. Angular Components, Modules, HTTP services (HttpClient), Routing, Third-party UI libraries, and concrete API implementations.

Entities (Domain Layer)

This layer defines the core business objects and rules of your application. In Angular, these are typically simple TypeScript interfaces or classes that represent your domain models, free from any framework-specific code.

  • Example: src/app/domain/models/user.model.ts
    export interface User {
      id: string;
      name: string;
      email: string;
      isActive: boolean;
    }

Use Cases (Application Layer)

The Use Cases layer contains the application's specific business logic. These are services that orchestrate the flow of data to achieve a particular application goal. They interact with entities and rely on interfaces (defined in this layer or entities) to communicate with outer layers.

  • Example: src/app/application/use-cases/get-users.usecase.ts

    import { Observable } from 'rxjs';
    import { User } from '../../domain/models/user.model';
    import { UserRepository } from '../../application/ports/user.repository'; // Interface defined here
    
    export class GetUsersUseCase {
      constructor(private userRepository: UserRepository) {}
    
      execute(): Observable<User[]> {
        return this.userRepository.getAllUsers();
      }
    }

Interface Adapters

This layer acts as a translator between the inner business logic and the external world. It includes gateways (interfaces for data persistence), presenters (to prepare data for the UI), and controllers (to receive input from the UI).

  • Example (Port/Interface for Repository): src/app/application/ports/user.repository.ts

    import { Observable } from 'rxjs';
    import { User } from '../../domain/models/user.model';
    
    export abstract class UserRepository {
      abstract getAllUsers(): Observable<User[]>;
      abstract getUserById(id: string): Observable<User>;
      // ... other methods
    }
  • Example (Concrete Implementation of Repository): src/app/infrastructure/api/user-api.repository.ts

    import { Injectable } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { Observable } from 'rxjs';
    import { User } from '../../domain/models/user.model';
    import { UserRepository } from '../../application/ports/user.repository';
    
    @Injectable({
      providedIn: 'root'
    })
    export class UserApiRepository implements UserRepository { // Implements the abstract class/interface
      private apiUrl = 'https://api.example.com/users';
    
      constructor(private http: HttpClient) {}
    
      getAllUsers(): Observable<User[]> {
        return this.http.get<User[]>(this.apiUrl);
      }
    
      getUserById(id: string): Observable<User> {
        return this.http.get<User>(`${this.apiUrl}/${id}`);
      }
    }

Frameworks & Drivers

This is the outermost layer, containing all the "details" like the UI (Angular components), database implementations, web frameworks, and external libraries. These components depend on the inner layers.

  • Example (Angular Component using a Use Case): src/app/presentation/users/user-list.component.ts

    import { Component, OnInit } from '@angular/core';
    import { Observable } from 'rxjs';
    import { User } from '../../domain/models/user.model';
    import { GetUsersUseCase } from '../../application/use-cases/get-users.usecase';
    
    @Component({
      selector: 'app-user-list',
      templateUrl: './user-list.component.html',
      styleUrls: ['./user-list.component.css']
    })
    export class UserListComponent implements OnInit {
      users$: Observable<User[]> | undefined;
    
      constructor(private getUsersUseCase: GetUsersUseCase) {} // Inject the Use Case
    
      ngOnInit(): void {
        this.users$ = this.getUsersUseCase.execute();
      }
    }

Benefits of Clean Architecture in Angular

Adopting Clean Architecture in Angular offers significant advantages for application development and long-term maintenance:

  • Modularity: Clearly defined layers and responsibilities make the application highly modular, allowing for independent development and deployment of different parts.
  • Testability: Because core business logic (entities and use cases) are independent of external frameworks, they can be tested in isolation with ease, leading to more robust and reliable tests.
  • Maintainability: Changes in one layer have minimal impact on others, simplifying debugging, refactoring, and updates. This reduces the risk of introducing new bugs.
  • Scalability: A decoupled architecture makes it easier to add new features or scale existing ones without affecting the core application.
  • Decoupling: Reduces coupling between components, making the system more flexible and adaptable to changes in requirements or technology.
  • Technology Independence: The core business rules are independent of Angular, a specific database, or any third-party UI library. This means you could theoretically swap out Angular for another framework if needed (though highly unlikely in practice, it illustrates the decoupling).

Practical Implementation in Angular

Implementing Clean Architecture effectively in an Angular project involves thoughtful folder structuring and adhering to the Dependency Rule during dependency injection.

Example Folder Structure

A common way to structure an Angular project following Clean Architecture principles might look like this:

  • src/app/
    • domain/
      • models/ (e.g., user.model.ts, product.model.ts)
      • constants/
    • application/
      • use-cases/ (e.g., get-users.usecase.ts, create-order.usecase.ts)
      • ports/ (abstract interfaces for repositories, services, e.g., user.repository.ts)
    • infrastructure/
      • api/ (concrete implementations of repositories using HttpClient, e.g., user-api.repository.ts)
      • storage/ (local storage, IndexedDB implementations)
      • config/
    • presentation/
      • components/ (UI components, e.g., user-list/, product-detail/)
      • pages/ (smart components that orchestrate use cases)
      • shared/ (common UI elements)
      • view-models/ (data structures optimized for specific views)
    • app.module.ts (root module for dependency injection configuration)

How Angular Elements Fit

  • Services: Angular services are central to implementing Clean Architecture.
    • Use Cases are implemented as services in the application layer.
    • Concrete API/database implementations are services in the infrastructure layer.
    • Ports (interfaces for repositories) are abstract classes or interfaces often defined in the application layer.
  • Components: Components belong to the presentation layer. They should primarily focus on displaying data and capturing user input. They inject and call use cases, but they do not contain business logic themselves.
  • Modules: Angular modules (NgModule) are used to organize the application into feature areas and manage dependency injection. You can create modules for each layer or feature domain to enforce encapsulation.

Considerations and Challenges

While beneficial, adopting Clean Architecture can introduce initial overhead and a steeper learning curve, especially for developers new to the paradigm. The increased number of files and abstractions can feel daunting at first. However, the long-term benefits in terms of maintainability, testability, and scalability often outweigh these initial challenges for complex or evolving applications.