Onion Architecture
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 implementationsThe 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');
}
}
The Onion Architecture aims for loose coupling and improved testability by organizing code into concentric layers. The innermost layer represents the domain/business logic, independent of any infrastructure concerns. Surrounding layers represent use cases, interfaces (ports), and finally, the infrastructure (databases, UI, etc.). Dependencies point inwards – inner layers define what outer layers need, but are unaware of how those needs are met. This promotes a clean separation of concerns.
Here, we model a simple order processing system. The Domain layer defines core entities like Order. The UseCases layer contains logic for creating and processing orders, depending on Domain entities but not infrastructure. The Ports (interfaces) define how use cases interact with external concerns. Finally, Infrastructure provides concrete implementations (like a dummy repository). Notice dependencies flow inwards via interfaces.
// Domain Layer (Innermost)
package domain
case class Order(id: Int, items: List[String], totalAmount: Double)
// Use Cases Layer
package usecases
import domain.Order
trait OrderService {
def createOrder(items: List[String]): Order
def processOrder(orderId: Int): Order
}
class DefaultOrderService(orderRepository: OrderRepository) extends OrderService {
override def createOrder(items: List[String]): Order = {
val total = items.size * 10.0 // Simple pricing
val order = Order(1, items, total)
orderRepository.save(order)
order
}
override def processOrder(orderId: Int): Order = {
// Business logic to process order
val order = orderRepository.findById(orderId)
order.copy(status = "Processed") //Assume Order has a status field
}
}
trait OrderRepository {
def save(order: Order): Order
def findById(orderId: Int): Order
}
// Ports Layer (Interfaces) - defined within Use Cases since these are what the use cases require
// Infrastructure Layer (Outermost)
package infrastructure
import usecases.OrderRepository
import domain.Order
class InMemoryOrderRepository extends OrderRepository {
private var orders: Map[Int, Order] = Map.empty
override def save(order: Order): Order = {
orders = orders + (order.id -> order)
order
}
override def findById(orderId: Int): Order = {
orders.get(orderId).getOrElse(throw new NoSuchElementException(s"Order with id $orderId not found"))
}
}
// Entry Point / Application
object Main extends App {
val repository = new InMemoryOrderRepository()
val orderService = new DefaultOrderService(repository)
val newOrder = orderService.createOrder(List("ItemA", "ItemB"))
println(s"Created order: $newOrder")
val processedOrder = orderService.processOrder(newOrder.id)
println(s"Processed order: $processedOrder")
}
The Onion Architecture aims for loose coupling by organizing code in concentric layers. The core layer contains business rules and entities, independent of any infrastructure. Surrounding layers handle interface adapters (like controllers) and infrastructure details (databases, UI). Dependencies point inward – infrastructure components depend on application logic, not the other way around. This promotes testability and maintainability, as core logic doesn’t need to know about external concerns.
This PHP example demonstrates a simplified Onion Architecture with an Entity, UseCases, and Interfaces directory. The UserController (interface adapter) depends on the UserUseCase (application layer). UserUseCase depends on UserEntity (domain layer) and, ultimately, a database interface (UserInterface) which is implemented by a concrete UserRepository (infrastructure layer). Dependency Injection is used to manage dependencies.
<?php
// --- Domain Layer ---
namespace App\Entity;
class UserEntity
{
public function __construct(public int $id, public string $name, public string $email) {}
}
// --- Use Cases Layer ---
namespace App\UseCases;
use App\Entity\UserEntity;
use App\Interfaces\UserInterface;
class UserUseCase
{
public function __construct(private UserInterface $userRepository) {}
public function getUserById(int $id): ?UserEntity
{
return $this->userRepository->findById($id);
}
public function createUser(string $name, string $email): UserEntity
{
$user = new UserEntity(0, $name, $email);
return $this->userRepository->create($user);
}
}
// --- Interfaces Layer ---
namespace App\Interfaces;
use App\Entity\UserEntity;
interface UserInterface
{
public function findById(int $id): ?UserEntity;
public function create(UserEntity $user): UserEntity;
}
// --- Infrastructure Layer ---
namespace App\Infrastructure;
use App\Entity\UserEntity;
use App\Interfaces\UserInterface;
class UserRepository implements UserInterface
{
// Simulate a database with an array
private array $users = [];
public function findById(int $id): ?UserEntity
{
foreach ($this->users as $user) {
if ($user->id === $id) {
return $user;
}
}
return null;
}
public function create(UserEntity $user): UserEntity
{
$user->id = count($this->users) + 1;
$this->users[] = $user;
return $user;
}
}
// --- Interface Adapter (Controller) ---
namespace App\Controllers;
use App\UseCases\UserUseCase;
class UserController
{
public function __construct(private UserUseCase $userUseCase) {}
public function show(int $id)
{
$user = $this->userUseCase->getUserById($id);
if ($user) {
return "User ID: " . $user->id . ", Name: " . $user->name . ", Email: " . $user->email;
} else {
return "User not found";
}
}
public function create(string $name, string $email)
{
$user = $this->userUseCase->createUser($name, $email);
return "User created with ID: " . $user->id;
}
}
// --- Usage (Bootstrapping) ---
$userRepository = new UserRepository();
$userUseCase = new UserUseCase($userRepository);
$userController = new UserController($userUseCase);
echo $userController->show(1) . "\n"; // Output: User not found
echo $userController->create("John Doe", "john.doe@example.com") . "\n"; // Output: User created with ID: 1
echo $userController->show(1) . "\n"; // Output: User ID: 1, Name: John Doe, Email: john.doe@example.com
?>
The Onion Architecture aims for loose coupling and high cohesion by organizing code into concentric layers. The innermost layer contains enterprise-wide business rules, completely independent of the outer layers. Moving outwards, we have Domain Models, Infrastructure (databases, UI, etc.), and finally, interfaces (e.g., Rails controllers) that initiate the process. Dependencies point inward; the core doesn’t depend on the periphery, ensuring that changes in UI or database don’t affect core business logic. This example focuses on the core and a simplified interface layer to illustrate the dependency rule.
# core/entities/product.rb
class Product
attr_reader :id, :name, :price
def initialize(id, name, price)
@id = id
@name = name
@price = price
end
def to_s
"Product: #{@name}, Price: #{@price}"
end
end
# core/services/product_service.rb
class ProductService
def initialize(product_repository)
@product_repository = product_repository
end
def get_product(id)
@product_repository.find(id)
end
def create_product(name, price)
@product_repository.save(Product.new(UUID.generate, name, price))
end
end
# core/ports/product_repository.rb
module ProductRepository
def find(id)
raise NotImplementedError
end
def save(product)
raise NotImplementedError
end
end
require 'uuid'
# infrastructure/memory_product_repository.rb
class MemoryProductRepository
include ProductRepository
def initialize
@products = {}
end
def find(id)
@products[id]
end
def save(product)
@products[product.id] = product
end
end
# interface/product_controller.rb
class ProductController
def initialize(product_service)
@product_service = product_service
end
def get_product(id)
product = @product_service.get_product(id)
if product
product.to_s
else
"Product not found"
end
end
def create_product(name, price)
@product_service.create_product(name, price)
"Product created"
end
end
# Usage (outside the layers - composition root)
repository = MemoryProductRepository.new
service = ProductService.new(repository)
controller = ProductController.new(service)
puts controller.create_product("Laptop", 1200)
puts controller.get_product(repository.instance_variable_get(:@products).keys.first)
The Onion Architecture organizes code into concentric layers, with core business logic at the center and infrastructure concerns at the outer layers. Dependencies point inwards – outer layers depend on inner layers, but inner layers have no knowledge of the outer ones. This promotes testability, maintainability, and flexibility.
This Swift implementation uses protocols to define layer boundaries. The Core layer contains entities and use cases (business logic). The Interface layer defines how other layers interact with the Core. The Framework layer (e.g., UIKit, networking) depends on the Interface and implements details. Entity is a simple struct, UseCase uses protocols for dependency injection, and ViewController (framework layer) uses the UseCase through its protocol. This strict dependency inversion is key to Onion Architecture and idiomatic Swift uses of protocols.
// Core - Entities
struct User {
let id: Int
let name: String
}
// Core - Use Cases
protocol UserRepository {
func getUser(id: Int) -> User?
}
protocol GetUserProfileUseCase {
func execute(userId: Int) -> String?
}
struct GetUserProfile: GetUserProfileUseCase {
private let userRepository: UserRepository
init(userRepository: UserRepository) {
self.userRepository = userRepository
}
func execute(userId: Int) -> String? {
guard let user = userRepository.getUser(id: userId) else {
return nil
}
return "User Profile: \(user.name)"
}
}
// Interface - Defines interaction with Core
protocol UserInterface {
func displayUserProfile(profile: String?)
}
// Framework Layer (UIKit)
class ViewController: UIInterface (superclass), UserInterface {
private let getUserProfileUseCase: GetUserProfileUseCase
init(getUserProfileUseCase: GetUserProfileUseCase) {
self.getUserProfileUseCase = getUserProfileUseCase
}
override func viewDidLoad() {
super.viewDidLoad()
displayUserProfile(profile: getUserProfileUseCase.execute(userId: 1))
}
func displayUserProfile(profile: String?) {
// Display the profile in a UILabel or similar
print(profile ?? "User not found")
}
}
// Framework - Concrete implementation (e.g., Data source)
class MockUserRepository: UserRepository {
func getUser(id: Int) -> User? {
if id == 1 {
return User(id: 1, name: "John Doe")
}
return nil
}
}
// Example Usage
let userRepository = MockUserRepository()
let getUserProfileUseCase = GetUserProfile(userRepository: userRepository)
let viewController = ViewController(getUserProfileUseCase: getUserProfileUseCase)
// When the view loads, it will print "User Profile: John Doe"
The Onion Architecture is a design pattern focused on separation of concerns, aiming to create a loosely coupled, testable, and maintainable system. The core business logic (Entities and Use Cases) resides in the innermost layers, independent of external frameworks or databases. Outer layers represent mechanisms like interfaces, controllers, and data persistence. Dependencies always point inwards – meaning inner layers know nothing about outer layers.
This Kotlin example demonstrates a simplified Onion Architecture. entities define the core data. usecases contain application-specific business rules using those entities. interfaces define ports for interacting with the use cases (e.g., a UserRepositoryPort). controllers (rest) expose endpoints, and adapters (repositories) implement the interfaces, connecting to external systems like a mock database. Kotlin’s data classes, interfaces, and concise syntax support this separation well, enhancing readability and testability. Dependency Injection (DI) is assumed for wiring up components in a real app.
// Entities (Innermost Layer)
data class User(val id: Int, val name: String)
// Use Cases (Application Layer)
interface UserService {
fun getUserById(id: Int): User?
fun createUser(name: String): User
}
class DefaultUserService(private val userRepository: UserRepositoryPort) : UserService {
override fun getUserById(id: Int): User? = userRepository.getUserById(id)
override fun createUser(name: String): User = userRepository.createUser(name)
}
// Interfaces (Domain Layer - Ports)
interface UserRepositoryPort {
fun getUserById(id: Int): User?
fun createUser(name: String): User
}
// Adapters (Infrastructure Layer)
class InMemoryUserRepository : UserRepositoryPort {
private val users = mutableListOf<User>()
override fun getUserById(id: Int): User? = users.find { it.id == id }
override fun createUser(name: String): User {
val newUser = User(users.size + 1, name)
users.add(newUser)
return newUser
}
}
// Controllers (Presentation Layer)
// (Simple example - in a real app, use a framework like Spring Boot or Ktor)
object UserController {
private val userService = DefaultUserService(InMemoryUserRepository())
fun getUser(id: Int): String {
val user = userService.getUserById(id) ?: return "User not found"
return "User: ${user.name}"
}
fun createUser(name: String): String {
val user = userService.createUser(name)
return "User created: ${user.name}"
}
}
// Example Usage
fun main() {
println(UserController.createUser("Alice"))
println(UserController.getUser(1))
println(UserController.getUser(2))
}
The Onion Architecture aims for a loose coupling between business logic and implementation details (like databases, UI, etc.). It structures the application in concentric layers: Domain (core business rules), Application (use cases orchestrating domain logic), and Infrastructure (details like persistence or messaging). Dependencies point inwards – inner layers know nothing of outer layers. This makes the core logic highly testable and resistant to changes in external dependencies.
This Rust example demonstrates a simplified Onion Architecture. The domain module holds core entities and logic. The application module defines services representing use cases that utilize the domain. Finally, infrastructure houses the actual repository implementation, and a basic main function acts as the entry point, orchestrating everything. The use of traits in Rust (e.g., UserRepository) allows the application layer to interact with data access without knowing the concrete implementation. This exemplifies Rust’s ownership and borrowing system fostering clear dependencies, a core principle of the pattern.
// domain/src/lib.rs
pub mod user;
// domain/src/user.rs
#[derive(Debug, Clone, PartialEq)]
pub struct User {
pub id: u32,
pub name: String,
}
// application/src/lib.rs
pub mod user_service;
// application/src/user_service.rs
pub trait UserRepository {
fn get_user(&self, id: u32) -> Option<User>;
fn create_user(&mut self, name: String) -> User;
}
pub struct UserService {
user_repository: &'static mut dyn UserRepository,
}
impl UserService {
pub fn new(user_repository: &'static mut dyn UserRepository) -> Self {
UserService { user_repository }
}
pub fn get_user_name(&self, id: u32) -> Option<String> {
self.user_repository.get_user(id).map(|user| user.name)
}
pub fn create_user_name(&mut self, name: String) -> String {
let user = self.user_repository.create_user(name);
user.name
}
}
// infrastructure/src/lib.rs
pub mod user_repository;
// infrastructure/src/user_repository.rs
use crate::User;
pub struct InMemoryUserRepository {
users: Vec<User>,
next_id: u32,
}
impl InMemoryUserRepository {
pub fn new() -> Self {
InMemoryUserRepository {
users: Vec::new(),
next_id: 1,
}
}
}
impl UserRepository for InMemoryUserRepository {
fn get_user(&self, id: u32) -> Option<User> {
self.users.iter().find(|user| user.id == id).cloned()
}
fn create_user(&mut self, name: String) -> User {
let user = User {
id: self.next_id,
name: name.clone(),
};
self.users.push(user.clone());
self.next_id += 1;
user
}
}
// src/main.rs
use application::user_service::{UserService, UserRepository};
use infrastructure::user_repository::InMemoryUserRepository;
fn main() {
let mut user_repository = InMemoryUserRepository::new();
let mut user_service = UserService::new(&mut user_repository as &mut dyn UserRepository);
let name1 = user_service.create_user_name("Alice".to_string());
println!("Created user name: {}", name1);
if let Some(name) = user_service.get_user_name(1) {
println!("User name: {}", name);
}
}
The Onion Architecture aims to achieve independence from frameworks, databases, and UI by structuring an application in concentric layers. The core business logic resides in the innermost layer (Entities), with dependencies pointing outwards. Outward layers represent infrastructure concerns like data access (Repositories) and presentation (Handlers). This promotes testability and maintainability as business rules aren’t coupled to external details.
The Go example demonstrates a simplified Onion Architecture. domain defines the core entities and business logic (services). infrastructure contains database repositories. application (or interfaces layer) defines interfaces for interacting with the domain, and implements use cases using the domain services and an interface-based repository. main handles request parsing, calls a use case, and presents the output—acting as the outermost layer and a starting point. This separation and dependency inversion are idiomatic in Go through interface usage.
// main.go - Outermost Layer (Presentation/Interface)
package main
import "fmt"
// UseCase defines the interface for our application logic.
type UseCase interface {
ProcessOrder(userID string, itemID string, quantity int) error
}
// handler is a simple struct to hold the use case.
type handler struct {
useCase UseCase
}
// NewHandler creates a new handler with the given use case.
func NewHandler(useCase UseCase) *handler {
return &handler{useCase: useCase}
}
// ProcessOrderRequest represents the input to the process order endpoint.
type ProcessOrderRequest struct {
UserID string
ItemID string
Quantity int
}
// ProcessOrder handles the process order request.
func (h *handler) ProcessOrder(req ProcessOrderRequest) error {
return h.useCase.ProcessOrder(req.UserID, req.ItemID, req.Quantity)
}
func main() {
// Assemble the layers (dependency injection)
// In a real app, the repository would be configured through DI as well
useCase := NewOrderService(NewOrderRepository())
h := NewHandler(useCase)
// Simulate a request
req := ProcessOrderRequest{UserID: "user123", ItemID: "item456", Quantity: 2}
// Process the order
err := h.ProcessOrder(req)
if err != nil {
fmt.Println("Error processing order:", err)
return
}
fmt.Println("Order processed successfully!")
}
// domain/entities.go - Core (Entities)
package domain
// OrderItem represents an item in an order.
type OrderItem struct {
ItemID string
Quantity int
}
// Order represents the core order entity.
type Order struct {
OrderID string
UserID string
Items []OrderItem
}
// domain/services.go - Core (Services)
package domain
// OrderService is responsible for the order-related business logic.
type OrderService interface {
ProcessOrder(userID string, itemID string, quantity int) error
}
type orderService struct {
orderRepository OrderRepository
}
func NewOrderService(orderRepository OrderRepository) OrderService {
return &orderService{orderRepository: orderRepository}
}
func (s *orderService) ProcessOrder(userID string, itemID string, quantity int) error {
// Business Logic: Validations, calculations, etc.
if quantity <= 0 {
return fmt.Errorf("invalid quantity: %d", quantity)
}
// Create order item
orderItem := OrderItem{ItemID: itemID, Quantity: quantity}
// Create order
order := Order{
OrderID: "order-" + userID + "-" + itemID, //Simple order ID
UserID: userID,
Items: []OrderItem{orderItem},
}
// Persist the order
return s.orderRepository.Save(order)
}
// domain/repositories.go - Core (Repository Interface)
package domain
import "fmt"
// OrderRepository defines the interface for interacting with order data.
type OrderRepository interface {
Save(order Order) error
}
// infrastructure/order_repository.go - Infrastructure (Data Access)
package infrastructure
import "fmt"
import "github.com/yourusername/onionarch/domain" // Replace with your module path
// OrderRepositoryImpl is a concrete implementation of the OrderRepository interface.
type OrderRepositoryImpl struct {
// Database connection or other data store
}
// NewOrderRepository creates a new OrderRepositoryImpl.
func NewOrderRepository() domain.OrderRepository {
return &OrderRepositoryImpl{}
}
// Save saves an order to the data store.
func (r *OrderRepositoryImpl) Save(order domain.Order) error {
// Simulate database saving.
fmt.Printf("Saving order to database: %+v\n", order)
return nil
}
The Onion Architecture aims for loose coupling and testability by organizing code into concentric layers. The innermost layer represents the core business logic (Entities), independent of any external concerns. Surrounding layers define Use Cases (application-specific business rules), Interface Adapters (translators like controllers and gateways), and finally, the infrastructure details (databases, UI). Dependencies point inwards; the core doesn’t know about the UI, but the UI knows about the core. This example simplifies this with a focus on separation and dependency inversion, as a full onion architecture is complex to represent concisely in C without frameworks.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// --- Core: Entities ---
typedef struct {
int id;
char name[50];
} Entity;
int create_entity(const char *name, Entity **new_entity) {
*new_entity = (Entity *)malloc(sizeof(Entity));
if (*new_entity == NULL) return 0;
(*new_entity)->id = rand(); // Simulate ID generation
strncpy((*new_entity)->name, name, sizeof((*new_entity)->name) - 1);
(*new_entity)->name[sizeof((*new_entity)->name) - 1] = '\0'; // Ensure null termination
return 1;
}
void free_entity(Entity *entity) {
free(entity);
}
// --- Use Cases ---
typedef struct {
void (*process_entity)(Entity *); // Function pointer for the use case
} UseCase;
void print_entity_details(Entity *entity) {
printf("Entity ID: %d, Name: %s\n", entity->id, entity->name);
}
UseCase entity_printer_use_case = { .process_entity = print_entity_details };
void execute_use_case(UseCase use_case, Entity *entity) {
use_case.process_entity(entity);
}
// --- Interface Adapters (Controller) ---
typedef struct {
UseCase use_case;
} Controller;
void handle_entity_request(Controller *controller, const char *entity_name) {
Entity *entity;
if (create_entity(entity_name, &entity)) {
execute_use_case(controller->use_case, entity);
free_entity(entity);
} else {
printf("Failed to create entity.\n");
}
}
// --- Infrastructure (Main) ---
int main() {
srand(12345); // Seed for random ID
Controller printer_controller = { .use_case = entity_printer_use_case };
handle_entity_request(&printer_controller, "Example Entity");
return 0;
}
The Onion Architecture aims for loose coupling by organizing code into concentric layers. The innermost layer represents the core business logic, independent of any external concerns (databases, UI, etc.). Layers outward represent infrastructure details, only depending on inner layers. This promotes testability and adaptability.
Our C++ example simplifies a basic user service. The Entities layer defines the User data structure. UseCases contain the business logic, operating on User objects. InterfaceAdapters (presenters/controllers - here just a simple controller) acts as a translator between external requests and use cases. Finally, the FrameworkDrivers layer (e.g., CLI, web framework) handles I/O and dependency injection. Forward declarations heavily used to manage dependencies and avoid circular includes, maintaining the architecture’s layering.
#include <iostream>
#include <string>
#include <vector>
// Entities (Core Business Objects)
namespace Entities {
struct User {
int id;
std::string name;
};
} // namespace Entities
// Use Cases (Business Logic)
namespace UseCases {
using Entities::User;
class UserService {
public:
std::vector<User> getAllUsers() {
// In a real application, this would fetch from a repository.
return {{1, "Alice"}, {2, "Bob"}};
}
};
} // namespace UseCases
// Interface Adapters (Presenters/Controllers)
namespace InterfaceAdapters {
namespace Controllers {
using Entities::User;
using UseCases::UserService;
class UserController {
public:
UserController(UserService& userService) : userService_(userService) {}
void listUsers() {
std::vector<User> users = userService_.getAllUsers();
for (const auto& user : users) {
std::cout << "User ID: " << user.id << ", Name: " << user.name << std::endl;
}
}
private:
UserService& userService_;
};
} // namespace Controllers
} // namespace InterfaceAdapters
// Framework Drivers (I/O, Dependencies)
int main() {
UseCases::UserService userService;
InterfaceAdapters::Controllers::UserController controller(userService);
std::cout << "Listing Users:" << std::endl;
controller.listUsers();
return 0;
}
The Onion Architecture aims for loose coupling and high testability by organizing code into concentric layers. The core layer contains business rules and entities, completely independent of infrastructure concerns. Outer layers represent interfaces, data access, and presentation. Dependencies point inward – infrastructure depends on application logic, but application logic knows nothing of infrastructure. This promotes adaptability; you can swap databases or UI frameworks without impacting core business rules.
This implementation defines interfaces in the Application and Infrastructure layers. The Domain layer houses entities and interfaces for use cases. The Startup layer (often a console app or ASP.NET core entry point) orchestrates dependencies. Dependency Injection (DI) is key, resolving dependencies against interface definitions. This structure embodies C#’s focus on strong typing and interface-based programming, facilitating testability and maintainability.
// Domain Layer - Core Business Logic
namespace Onion.Domain.Models;
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}
namespace Onion.Domain.Interfaces;
public interface IProductRepository
{
Product GetProduct(int id);
void AddProduct(Product product);
}
// Application Layer - Use Cases
namespace Onion.Application.Services;
public class ProductService
{
private readonly IProductRepository _productRepository;
public ProductService(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public Product GetProductDetails(int id)
{
return _productRepository.GetProduct(id);
}
public void AddNewProduct(Product product)
{
_productRepository.AddProduct(product);
}
}
// Infrastructure Layer - Data Access (e.g., EF Core)
namespace Onion.Infrastructure.Repositories;
using Onion.Domain.Interfaces;
using Onion.Domain.Models;
public class ProductRepository : IProductRepository
{
// Simulate a database context
private static List<Product> _products = new()
{
new Product { Id = 1, Name = "Example Product", Price = 19.99m }
};
public Product GetProduct(int id)
{
return _products.FirstOrDefault(p => p.Id == id);
}
public void AddProduct(Product product)
{
_products.Add(product);
}
}
// Startup / Composition Root (Console App example)
namespace Onion.Startup;
using Microsoft.Extensions.DependencyInjection;
using Onion.Application.Services;
using Onion.Domain.Interfaces;
using Onion.Infrastructure.Repositories;
public static class Program
{
public static void Main(string[] args)
{
var services = new ServiceCollection();
// Register Application Services
services.AddScoped<ProductService>();
// Register Infrastructure (implementations pointing to interfaces)
services.AddScoped<IProductRepository, ProductRepository>();
var serviceProvider = services.BuildServiceProvider();
// Use the services
var productService = serviceProvider.GetRequiredService<ProductService>();
var product = productService.GetProductDetails(1);
Console.WriteLine($"Product Name: {product?.Name}, Price: {product?.Price}");
productService.AddNewProduct(new Domain.Models.Product{Id = 2, Name = "New Product", Price = 29.99m});
var newProduct = productService.GetProductDetails(2);
Console.WriteLine($"New Product Name: {newProduct?.Name}, Price: {newProduct?.Price}");
}
}
The Onion Architecture aims for loose coupling and high cohesion by organizing code into concentric layers. The innermost layer represents the Enterprise Rules (core business logic, entities). Middle layers are Interface Adapters (controllers, presenters) that translate data to and from the core. The outermost layer is Infrastructure (databases, UI, third-party libraries). Dependencies point inwards – infrastructure concerns depend on application logic, but the application logic doesn’t depend on infrastructure details. This promotes testability and makes the core resistant to changes in outer layers.
This TypeScript implementation showcases a simplified Onion Architecture. The entities directory holds core domain models. use-cases or application represent the application’s use cases and depends on entities. interfaces converts use case outputs for presentation or calling services. infrastructure provides concrete implementations for external concerns like data access. Dependency Injection is used for loose coupling. The code adopts standard TypeScript module structure and naming conventions.
// entities/user.ts
export interface User {
id: string;
name: string;
email: string;
}
// use-cases/get-user.ts
import { User } from '../entities/user';
import { UserRepository } from '../interfaces/user-repository';
export interface GetUserUseCase {
getUser(id: string): Promise<User | null>;
}
export class GetUser implements GetUserUseCase {
constructor(private userRepository: UserRepository) {}
async getUser(id: string): Promise<User | null> {
return this.userRepository.getUserById(id);
}
}
// interfaces/user-repository.ts
import { User } from '../entities/user';
export interface UserRepository {
getUserById(id: string): Promise<User | null>;
}
// infrastructure/in-memory-user-repository.ts
import { UserRepository } from '../interfaces/user-repository';
import { User } from '../entities/user';
export class InMemoryUserRepository implements UserRepository {
private users: User[] = [
{ id: '1', name: 'Alice', email: 'alice@example.com' },
{ id: '2', name: 'Bob', email: 'bob@example.com' },
];
async getUserById(id: string): Promise<User | null> {
const user = this.users.find((u) => u.id === id);
return user || null;
}
}
// interfaces/user-controller.ts
import { GetUserUseCase } from '../use-cases/get-user';
export interface UserController {
getUser(id: string): Promise<string>;
}
// interfaces/user-presenter.ts
import { User } from '../entities/user';
export interface UserPresenter {
presentUser(user: User): string;
}
// infrastructure/console-user-presenter.ts
export class ConsoleUserPresenter implements UserPresenter {
presentUser(user: User): string {
return `User ID: ${user.id}, Name: ${user.name}, Email: ${user.email}`;
}
}
// interfaces/user-service.ts
export interface UserService {
getUserDetails(id: string): Promise<string>;
}
// infrastructure/user-service.ts
import { GetUserUseCase } from '../use-cases/get-user';
import { UserPresenter } from '../interfaces/user-presenter';
export class DefaultUserService implements UserService {
constructor(private getUserUseCase: GetUserUseCase, private userPresenter: UserPresenter) {}
async getUserDetails(id: string): Promise<string> {
const user = await this.getUserUseCase.getUser(id);
if (user) {
return this.userPresenter.presentUser(user);
} else {
return `User with ID ${id} not found.`;
}
}
}
// main.ts - Entry Point (Outer Layer)
import { InMemoryUserRepository } from './infrastructure/in-memory-user-repository';
import { GetUser } from './use-cases/get-user';
import { DefaultUserService } from './infrastructure/user-service';
import { ConsoleUserPresenter } from './infrastructure/console-user-presenter';
async function main() {
const userRepository = new InMemoryUserRepository();
const getUserUseCase = new GetUser(userRepository);
const userPresenter = new ConsoleUserPresenter();
const userService = new DefaultUserService(getUserUseCase, userPresenter);
const userId = '1';
const userDetails = await userService.getUserDetails(userId);
console.log(userDetails); // Output: User ID: 1, Name: Alice, Email: alice@example.com
}
main();
The Onion Architecture aims to achieve a loose coupling between business logic and infrastructure concerns like databases or UI frameworks. It structures the application in concentric layers: Domain (core business rules), Application (use cases orchestrating domain models), and Infrastructure (external implementations like data access). Dependencies point inward – infrastructure knows about application and domain, but domain knows nothing of infrastructure. This allows changes in infrastructure without impacting core business logic.
Here, we’ll represent a simplified system with a user service using this structure. The Domain layer defines a User entity. The Application layer defines a UserService that uses the User entity to perform business logic (like creating a user). The Infrastructure layer provides a concrete implementation for user storage (here, an in-memory repository; in a real app, this could be a database). Dependency Injection is used to couple the layers. This aligns with JavaScript’s flexible nature, allowing clear separation of concerns and testability.
// Domain Layer
class User {
constructor(id, name, email) {
this.id = id;
this.name = name;
this.email = email;
}
isValid() {
return this.name && this.email;
}
}
// Application Layer
class UserService {
constructor(userRepository) {
this.userRepository = userRepository;
}
createUser(name, email) {
const user = new User(Math.random(), name, email);
if (user.isValid()) {
this.userRepository.save(user);
return user;
}
return null;
}
getUserById(id) {
return this.userRepository.getById(id);
}
}
// Infrastructure Layer
class InMemoryUserRepository {
constructor() {
this.users = [];
}
save(user) {
this.users.push(user);
}
getById(id) {
return this.users.find(user => user.id === id) || null;
}
}
// Composition Root (Entry Point)
const userRepository = new InMemoryUserRepository();
const userService = new UserService(userRepository);
// Example Usage
const newUser = userService.createUser("John Doe", "john.doe@example.com");
if (newUser) {
console.log("Created User:", newUser);
}
const retrievedUser = userService.getUserById(newUser.id);
if (retrievedUser) {
console.log("Retrieved User:", retrievedUser);
}
The Onion Architecture aims for loose coupling and high cohesion by organizing code into concentric layers. The core layer contains business rules, independent of any external concerns. Surrounding layers represent interfaces (ports) to adapt these rules to specific technologies like databases or UI frameworks. Dependencies always point inwards – outer layers depend on inner layers, not vice versa. This facilitates testability, maintainability, and adaptability.
The Python example uses packages to represent layers: core (entities & use cases), interfaces (ports/abstract base classes), and infrastructure (adapters for specific technologies). The infrastructure layer depends on interfaces and (through interfaces) on core. This maintains the dependency inversion principle central to the Onion Architecture. Adopting a package-based approach is idiomatic Python for structuring larger projects and promotes clarity.
# -*- coding: utf-8 -*-
"""
Onion Architecture Example in Python
"""
# core/entities.py
class User:
def __init__(self, user_id, name):
self.user_id = user_id
self.name = name
# core/use_cases.py
from .entities import User
class UserService:
def __init__(self, user_repository):
self.user_repository = user_repository
def get_user(self, user_id):
return self.user_repository.get(user_id)
# interfaces/user_repository.py
from abc import ABC, abstractmethod
class UserRepository(ABC):
@abstractmethod
def get(self, user_id):
pass
# infrastructure/database_user_repository.py
from interfaces.user_repository import UserRepository
class DatabaseUserRepository(UserRepository):
def __init__(self, db_connection):
self.db_connection = db_connection
def get(self, user_id):
# Simulate database interaction
if user_id == 1:
return User(1, "Alice")
else:
return None
# application.py (Entry Point / Composition Root)
from core.use_cases import UserService
from infrastructure.database_user_repository import DatabaseUserRepository
def configure_repositories():
# Simulate database connection
db_connection = "some_db_connection_string"
return DatabaseUserRepository(db_connection)
def main():
user_repository = configure_repositories()
user_service = UserService(user_repository)
user = user_service.get_user(1)
if user:
print(f"User ID: {user.user_id}, Name: {user.name}")
else:
print("User not found")
if __name__ == "__main__":
main()
The Onion Architecture is a design pattern focused on achieving loose coupling and increased testability by organizing code into concentric layers. The innermost layer represents the domain – core business logic with no dependencies. Surrounding layers represent domain services, data access, and infrastructure (UI, frameworks, etc.), each depending only on the layers within. Changes in outer layers shouldn’t force changes in inner layers.
This Java example demonstrates a simplified Onion Architecture with Domain, Use Case (Application Service), and Infrastructure layers. Interfaces define layer boundaries, allowing dependency inversion. The core domain objects (Product) are pure Java objects without framework dependencies. Use cases use these domain objects to implement business rules. Infrastructure handles persistence (here, a simple in-memory repository) and interacts with the external world. This is idiomatic Java due to its use of interfaces for abstraction, dependency injection (although simple here), and the clear separation of concerns promoted by the architecture.
// Domain Layer
package com.example.onion.domain;
public class Product {
private String id;
private String name;
private double price;
public Product(String id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
}
// Use Case Layer
package com.example.onion.usecase;
import com.example.onion.domain.Product;
import com.example.onion.domain.ProductRepository;
public class GetProduct {
private final ProductRepository productRepository;
public GetProduct(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public Product getProductById(String id) {
return productRepository.findById(id);
}
}
// Infrastructure Layer
package com.example.onion.infrastructure;
import com.example.onion.domain.Product;
import com.example.onion.domain.ProductRepository;
import java.util.HashMap;
import java.util.Map;
public class InMemoryProductRepository implements ProductRepository {
private final Map<String, Product> products = new HashMap<>();
public InMemoryProductRepository() {
products.put("1", new Product("1", "Laptop", 1200.0));
products.put("2", new Product("2", "Mouse", 25.0));
}
@Override
public Product findById(String id) {
return products.get(id);
}
}
// Main (For demonstration - typically handled by a framework)
package com.example.onion;
import com.example.onion.usecase.GetProduct;
import com.example.onion.infrastructure.InMemoryProductRepository;
import com.example.onion.domain.Product;
public class Main {
public static void main(String[] args) {
ProductRepository repository = new InMemoryProductRepository();
GetProduct getProduct = new GetProduct(repository);
Product product = getProduct.getProductById("1");
if (product != null) {
System.out.println("Product Name: " + product.getName());
System.out.println("Product Price: " + product.getPrice());
} else {
System.out.println("Product not found!");
}
}
}