034
architectural design principles DDD

Clean Architecture

Reference Wikipedia ↗
Clean Architecture — component diagram
Plate 034 component diagram

Clean Architecture is a software design philosophy that emphasizes separation of concerns to achieve high modularity, testability, and maintainability. It proposes structuring an application into concentric layers, with core business logic residing in the innermost layers and external concerns like databases, UI frameworks, and external APIs residing in the outermost layers. Dependencies point inwards, meaning inner layers have no knowledge of outer layers, promoting independence from technology changes and simplifying testing.

The primary goal of Clean Architecture is to create systems that are independent of frameworks, databases, UI, and any external agency. This independence allows for easier adaptation to changing requirements, improved testability (as business logic can be tested in isolation), and increased flexibility in choosing and swapping out technologies without impacting the core application. It achieves this through a strict dependency rule: source code dependencies can only point inwards.

Usage

Clean Architecture is commonly used in:

  • Large, complex applications: Where maintainability and adaptability are crucial over the long term.
  • Applications with evolving requirements: The decoupled nature allows for changes in one area without cascading effects.
  • Systems requiring high testability: Inner layers can be tested easily without reliance on external dependencies.
  • Microservices architecture: Each microservice can be built on Clean Architecture principles for better isolation and independence.
  • Mobile Applications: When needing to support multiple platforms (iOS, Android) with shared core logic.

Examples

  • Hexagonal Architecture (Ports and Adapters): Often considered a specific implementation of Clean Architecture, Hexagonal Architecture, used in many Java and .NET projects, explicitly defines ports (interfaces) that core logic interacts with, and adapters that connect those ports to external systems. Spring Framework often encourages this pattern through its dependency injection capabilities.
  • Onion Architecture: Similar to Clean Architecture, Onion Architecture focuses on placing the core domain logic at the center and building layers of infrastructure around it. ASP.NET Core projects frequently adopt this structure, separating concerns into domain models, application services, and infrastructure layers.
  • SwiftUI and Combine (Apple Ecosystem): Apple’s SwiftUI and Combine frameworks, while not explicitly enforcing Clean Architecture, lend themselves well to it. The MVVM (Model-View-ViewModel) pattern, often used with these frameworks, can be implemented within the Clean Architecture layers, with the ViewModel residing in the Use Cases layer and the Model representing Entities.
  • Flask/Django with Core Logic Separation (Python Web Frameworks): Python web frameworks like Flask and Django can be structured to follow Clean Architecture principles. The core business logic is placed in separate modules, independent of the web framework’s specifics, allowing for easier testing and potential migration to other frameworks.

Specimens

15 implementations
Specimen 034.01 Dart View specimen ↗

Clean Architecture aims to create systems independent of frameworks, databases, UI, and any external agency. It achieves this by organizing code into concentric layers: Entities (core business logic), Use Cases (application-specific logic), Interface Adapters (presenters, controllers, gateways), and Frameworks & Drivers (UI, databases). Dependencies point inwards – outer layers depend on inner layers, not the reverse. This makes the system testable, maintainable, and adaptable.

The Dart example below demonstrates a simplified Clean Architecture. entities/user.dart defines the core User entity. use_cases/login.dart contains the login use case, depending only on the entity. interface_adapters/login_controller.dart adapts the use case to a simple API. A console_app.dart represents the “Frameworks & Drivers” layer, handling user input and output in console format. Dart’s strong typing and support for both OOP and functional paradigms make it well-suited for this structure. Dependency Injection is implied through constructor parameters, adhering to the dependency inversion principle.

// entities/user.dart
class User {
  final String username;
  final String password;

  User(this.username, this.password);

  bool isValidPassword(String password) {
    return this.password == password;
  }
}

// use_cases/login.dart
abstract class LoginUseCase {
  Future<User?> execute(String username, String password);
}

class LoginUseCaseImpl implements LoginUseCase {
  final List<User> users;

  LoginUseCaseImpl(this.users);

  @override
  Future<User?> execute(String username, String password) async {
    await Future.delayed(Duration.zero); // Simulate async operation
    for (final user in users) {
      if (user.username == username && user.isValidPassword(password)) {
        return user;
      }
    }
    return null;
  }
}


// interface_adapters/login_controller.dart
abstract class LoginController {
  Future<String> login(String username, String password);
}

class LoginControllerImpl implements LoginController {
  final LoginUseCase loginUseCase;

  LoginControllerImpl(this.loginUseCase);

  @override
  Future<String> login(String username, String password) async {
    final user = await loginUseCase.execute(username, password);
    if (user != null) {
      return 'Login successful for ${user.username}';
    } else {
      return 'Login failed';
    }
  }
}

// console_app.dart (Frameworks & Drivers)
import 'dart:io';

import 'package:clean_architecture_dart/entities/user.dart';
import 'package:clean_architecture_dart/use_cases/login.dart';
import 'package:clean_architecture_dart/interface_adapters/login_controller.dart';

void main() async {
  final users = [User('user1', 'pass1'), User('user2', 'pass2')];
  final loginUseCase = LoginUseCaseImpl(users);
  final loginController = LoginControllerImpl(loginUseCase);

  print('Enter username:');
  final username = stdin.readLineSync() ?? '';

  print('Enter password:');
  final password = stdin.readLineSync() ?? '';

  final result = await loginController.login(username, password);
  print(result);
}