131
architectural DDD

Onion Architecture

Reference Wikipedia ↗
Onion Architecture — class diagram
Plate 131 class diagram

Onion Architecture is a software design pattern that advocates for separating concerns into distinct layers, with the core business logic residing at the very center. This central core is independent of any external concerns like databases, UI frameworks, or external services. Layers represent different levels of abstraction, and dependencies point inward – outer layers depend on inner layers, but inner layers have no knowledge of outer layers.

This architecture promotes testability, maintainability, and flexibility. By isolating the domain logic, changes to infrastructure or presentation layers don’t impact the core functionality. It’s particularly useful in complex applications where business rules are expected to evolve independently of the technology stack.

Usage

Onion Architecture is commonly used in:

  • Enterprise Applications: Where complex business rules and long-term maintainability are crucial.
  • Microservices: To ensure each service has a well-defined core and can be adapted to different technologies without affecting other services.
  • Domain-Driven Design (DDD) Projects: It provides a natural structure for implementing DDD principles, keeping the domain model pure and independent.
  • Applications requiring high testability: The decoupled nature of the layers makes unit testing much easier.

Examples

  • Hexagonal Architecture (Ports and Adapters): Often considered a close relative, Hexagonal Architecture shares the same core principles of dependency inversion and isolating the domain. Many .NET projects utilizing DDD adopt a variation of Onion Architecture, sometimes referred to as “Clean Architecture” which is heavily influenced by Robert C. Martin’s work and builds upon the Onion Architecture principles.
  • ASP.NET Core with MediatR: A typical implementation involves a Core layer containing entities and interfaces, a Domain layer with business logic, an Application layer using MediatR for commands and queries, and an Infrastructure layer for database access and external service integrations. The Presentation layer (e.g., an ASP.NET Core API) then interacts with the Application layer.

Specimens

15 implementations
Specimen 131.01 Dart View specimen ↗

The Onion Architecture aims to achieve separation of concerns by organizing code into concentric layers. The innermost layer represents the core business logic (Entities), followed by Use Cases (interacting with Entities), then Interface Adapters (translating data between Use Cases and external frameworks), and finally, the outermost layer holds frameworks and drivers like databases, UI, or APIs. Dependencies point inward – outer layers depend on inner layers, but inner layers have no knowledge of the outer ones. This promotes testability and loose coupling.

The Dart example demonstrates a simplified onion architecture. entities define core business models. use_cases contain application-specific business rules operating on entities. interface_adapters map data between use cases and the external main layer, representing a console application for simplicity. Data transfer objects (DTOs) help decouple entities from presentation. This structure favors composition over inheritance, a common Dart/Flutter practice, and allows for easy swapping of external dependencies (e.g., UI framework) without affecting core logic.

// entities/user.dart
class User {
  final String id;
  final String name;

  User({required this.id, required this.name});
}

// use_cases/get_user.dart
abstract class GetUserUseCase {
  Future<User?> getUser(String id);
}

class GetUser implements GetUserUseCase {
  final UserRepository userRepository;

  GetUser({required this.userRepository});

  @override
  Future<User?> getUser(String id) async {
    return await userRepository.findById(id);
  }
}

// interface_adapters/user_repository_interface.dart
abstract class UserRepository {
  Future<User?> findById(String id);
}

// interface_adapters/user_repository_impl.dart (Framework/Driver - e.g., a real database)
class UserRepositoryImpl implements UserRepository {
  // Simulate a database
  final Map<String, User> _users = {
    '1': User(id: '1', name: 'Alice'),
    '2': User(id: '2', name: 'Bob'),
  };

  @override
  Future<User?> findById(String id) async {
    await Future.delayed(Duration(milliseconds: 50)); // Simulate DB latency
    return _users[id];
  }
}

// Data Transfer Object (DTO) - simplifies data exchange
class UserDto {
  final String id;
  final String name;

  UserDto({required this.id, required this.name});

  factory UserDto.fromUser(User user) => UserDto(id: user.id, name: user.name);
}

// main.dart (Outer Layer - Frameworks & Drivers)
import 'package:onion_architecture_dart/use_cases/get_user.dart';
import 'package:onion_architecture_dart/interface_adapters/user_repository_impl.dart';
import 'package:onion_architecture_dart/interface_adapters/user_repository_interface.dart';
import 'package:onion_architecture_dart/entities/user.dart';

Future<void> main() async {
  final userRepository = UserRepositoryImpl();
  final getUserUseCase = GetUser(userRepository: userRepository);

  final user = await getUserUseCase.getUser('1');

  if (user != null) {
    final userDto = UserDto.fromUser(user);
    print('User ID: ${userDto.id}, Name: ${userDto.name}');
  } else {
    print('User not found');
  }
}