2022-11-21

Onion Architecture

What is onion Architecture

Onion Architecture is an architectural pattern for designing software applications. The architecture is visually depicted in concentric layers resembling an onion, hence the name. This pattern enforces a strict dependency control, where dependencies flow inward, while the interactions occur from outer layers towards the center.

The architecture’s primary objective is to tackle common pitfalls in software design, such as tight coupling and separation of concerns, enabling maintainability and adaptability in an application's structure.

Principles

Onion Architecture is based on a number of guiding principles:

  • The Domain is the Core
    The heart of the application is the domain, encompassing the fundamental business rules and logic. This includes business entities and value objects that are critical to the application's functioning. In Onion Architecture, domain entities do not depend on anything else. They are pure, in that they contain the properties and methods that are related directly to what they represent.

  • Dependence Inwards
    The layers of the architecture can only depend on layers inwards and never on those outwards. The idea is to keep the domain and the application layer independent of external concerns like UI, Infrastructure, or Tests. By making sure the coupling is only towards the center, the architecture allows outer layers to be easily interchangeable and modifiable without affecting the inner layers.

  • Isolation from Infrastructure
    Another principle of Onion Architecture is the encapsulation of infrastructure concerns within the outer layers. As such, any changes in the infrastructure, such as a change in database software or user interface, do not affect the organization and operation of inner layers. This approach facilitates the testing of core components without the need for infrastructure-related dependencies.

  • Application Core Code Independency
    All application core code can be tested without any user interface, database, web server, or any external element. By focusing on the core, the application becomes easier to maintain, more scalable, and less prone to errors.

Layers in Onion Architecture

In Onion Architecture, the application is divided into several layers with different responsibilities, each communicating with the layer directly inside or outside of it.

Onion architecture
Onion Architecture

Domain Model Layer

The domain model layer lies at the heart of the Onion Architecture. This innermost layer encapsulates the business rules, policies, and entities that are crucial to the application's domain. The domain model consists of business entities and value objects that directly represent the real-world concepts and behaviors applicable to the domain.

Domain objects should not contain any references to external concerns such as databases or UI, thereby making them persistence ignorant. They are pure in the sense that they encapsulate the essential properties and behaviors related to the business domain.

Domain Services Layer

Just outside the domain model layer is the domain services layer. The services within this layer handle business operations that involve multiple domain entities or value objects. They encapsulate business logic that doesn't naturally fit within domain objects.

Domain services might be used to coordinate tasks between several entities, perform complex calculations, or enforce business rules that span multiple entities. Like domain objects, domain services should remain isolated from infrastructure concerns.

Application Services Layer

Application services, also known as use cases, orchestrate the interaction between the domain and the outside world. They don't contain business rules or knowledge, but they are responsible for tasks such as transaction management and domain events triggering.

Application services coordinate the domain layer and the infrastructure layer. They call upon domain services and domain entities to perform operations related to business rules, and they interact with the infrastructure layer to handle tasks such as persistence, caching, or message sending.

Infrastructure, Tests, and User Interface Layer

The outermost layer in the Onion Architecture contains elements such as the user interface, tests, and infrastructure tasks. These are areas of the software that are prone to change over time due to evolving technology and requirements. As such, they are kept separate from the core business rules, thereby ensuring that the core layers remain unaffected by changes in the outer layers.

The Infrastructure sub-layer encapsulates concerns such as data persistence and network communication, typically through repositories and data access objects.

The Tests sub-layer consists of all the tests that drive the development and ensure the correctness of the application. This includes unit tests, integration tests, and end-to-end tests.

Finally, the User Interface sub-layer handles all user interactions, including presentation logic and user input handling. This could include web interfaces, REST APIs, desktop applications, and more.

Implementing Onion Architecture in Python

Implementing the Onion Architecture in Python involves defining classes that represent the different layers of the architecture: Domain Model, Domain Services, Application Services, and Infrastructure. Let's dive into how we could structure a simple application following the Onion Architecture.

Before proceeding, let's consider a hypothetical domain, such as a basic order processing system.

Domain Model Layer

First, let's define the domain entities. In our example, these could be Order and Product classes. These classes contain no logic related to infrastructure or application services, focusing solely on business logic.

domain_model.py
class Product:
    def __init__(self, id, name, price):
        self.id = id
        self.name = name
        self.price = price

class Order:
    def __init__(self, id, product, quantity):
        self.id = id
        self.product = product
        self.quantity = quantity
        self.total = self.calculate_total()

    def calculate_total(self):
        return self.product.price * self.quantity

Domain Services Layer

Next, we implement domain services. These services contain operations related to business logic that involves more than one domain entity. In our case, let's consider a DiscountService that calculates a discount for an order.

domain_services.py
class DiscountService:
    def apply_discount(self, order, discount_percentage):
        if discount_percentage < 0 or discount_percentage > 100:
            raise ValueError("Invalid discount percentage")

        order.total -= order.total * (discount_percentage / 100)
        return order

Application Services Layer

The application services layer serves as a bridge between the domain and the infrastructure. In our case, an OrderProcessingService could be responsible for creating orders and applying discounts.

application_services.py
from domain_services import DiscountService

class OrderProcessingService:
    def __init__(self, order_repository, product_repository):
        self.order_repository = order_repository
        self.product_repository = product_repository
        self.discount_service = DiscountService()

    def process_order(self, order_id, product_id, quantity, discount_percentage):
        product = self.product_repository.get(product_id)
        order = Order(order_id, product, quantity)
        self.discount_service.apply_discount(order, discount_percentage)
        self.order_repository.save(order)

Infrastructure Layer

The infrastructure layer can include a repository for accessing data from the database. In a real-world scenario, this might involve querying a SQL or NoSQL database, but for simplicity, we'll just use an in-memory list.

infrastructure.py
class OrderRepository:
    def __init__(self):
        self.orders = []

    def save(self, order):
        self.orders.append(order)

class ProductRepository:
    def __init__(self):
        self.products = []

    def get(self, product_id):
        for product in self.products:
            if product.id == product_id:
                return product

References

https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/
https://medium.com/expedia-group-tech/onion-architecture-deed8a554423
https://dev.to/barrymcauley/onion-architecture-3fgl
https://iktakahiro.dev/python-ddd-onion-architecture
https://github.com/iktakahiro/dddpy
https://github.com/microsoft/cookiecutter-python-flask-clean-architecture/blob/main/docs/onion-architecture-article.md

Ryusei Kakujo

researchgatelinkedingithub

Focusing on data science for mobility

Bench Press 100kg!