EXPEDIA GROUP TECHNOLOGY —ENGINEERING

Onion Architecture

Let’s slice it like a Pro

Ritesh Kapoor
Expedia Group Technology

--

Domain-driven design (DDD) is an approach to developing software for complex needs by deeply connecting the implementation to an evolving model of the core business concepts.

The domain is a sphere of knowledge. It refers to the business knowledge that our software is trying to model. Domain-Driven Design centres on the domain model that has a rich understanding of the processes and rules of a domain. Onion architecture implements this concept and dramatically increases code quality, reduces complexity and enables evolutionary enterprise systems.

Onion Architecture illustrating different layers. Domain Model at the centre, enclosed by Domain Services. Domain Services enclosed by Application services and then Infrastructure Services. Application enclosing all the layers. Observability services for monitoring the application
Onion Architecture

Why Onion Architecture?

Domain entities are the core and centre part. Onion architecture is built on a domain model in which layers are connected through interfaces. The idea is to keep external dependencies as far outward as possible where domain entities and business rules form the core part of the architecture.

  • It provides flexible, sustainable and portable architecture.
  • Layers are not tightly coupled and have a separation of concerns.
  • It provides better maintainability as all the code depends on deeper layers or the centre.
  • Improves overall code testability as unit tests can be created for separate layers without impacting other modules.
  • Frameworks/technologies can be easily changed without impacting the core domain. e.g. RabbitMQ can be replaced by ActiveMQ, SQL can be replaced by MongoDB

Principles

Onion Architecture is comprised of multiple concentric layers interfacing with each other towards the core that represents the domain. It is based on the inversion of control principle. The architecture does not focus on underlying technology or frameworks but the actual domain models. It is based on the following principles.

Dependency

The circles represent different layers of responsibility. In general, the deeper we dive, the closer we get to the domain and business rules. The outer circles represent mechanisms and the inner circles represent core domain logic. The outer layers depend on inner layers and the inner layers are completely unaware of outer circles. Classes, methods, variables, and source code in general belonging to the outer circle depends on the inner circle but not vice versa.

Data formats/structures may vary from layers. Outer layer data formats should not be used by inner layers. E.g. Data formats used in an API can vary from those used in a DB for persistence. Data flow can use data transfer objects. Whenever data crosses layers/boundaries, it should be in a form that is convenient for that layer. E.g. API’s can have DTO’s, DB layer can have Entity Objects depending on how objects stored in a database vary from the domain model.

Data encapsulation

Each layer/circle encapsulates or hides internal implementation details and exposes an interface to the outer layer. All layers also need to provide information that is conveniently consumed by inner layers. The goal is to minimize coupling between layers and maximize coupling within a vertical slice across layers. We define abstract interfaces at deeper layers and provide their concrete implementation at the outermost layer. This ensures we focus on the domain model without worrying too much about implementation details. We can also use dependency injection frameworks, like Spring, to connect interfaces with implementation at runtime. E.g. Repositories used in the domain and external services used in Application Services are implemented at the infrastructure layer.

Concentric circles depicting an external service interface exposed to the Application layer and implemented at the Infrastructure layer. Also shows a Repository interface exposed to Domain Model and implemented at Infrastructure layer.
Data Encapsulation in Onion Architecture

Separation of concerns

Application is divided into layers where each layer has a set of responsibilities and addresses separate concerns. Each layer acts as modules/package/namespace within the application.

Coupling

Low coupling in which one module interacts with another module and does not need to be concerned with the other module’s internals. All the internal layers need not be concerned about internal implementation of external layers.

Onion Architecture Layers

Let’s understand different layers of the architecture and their responsibilities with an order creation use case. When receiving a create order request, we would like to validate the order, save the order in the database, update inventory for all order items, debit order amount and lastly send a notification to the customer about order completion.

Package Diagram Illustrating Dependency across Layers. Application Services depends on Domain Services. Infrastructure services depending on Domain and Application Services.
Package Diagram Illustrating Dependency across Layers
Example Interactions Across Different Layers. Infrastructure Services like GRPC Server calling Application Services for Creating Order and orchestrating order creation across different Domain Services. Domain Services could be storing information in database, providing Interface for Inventory or Notification Service.

Domain Model/Entities

Domain Entities are the fundamental building block of Domain-Driven Design and they’re used to model concepts of your Ubiquitous Language in code. Entities are Domain concepts that have a unique identity in the problem domain. Domain entities encapsulate attributes and entity behaviour. It is supposed to be independent of specific technologies like databases or web APIs. E.g. In the Orders domain. Order is an entity and has attributes like OrderId, Address, UserInfo, OrderItems, PricingInfo and behaviour like AddOrderItems, GetPricingInfo, ValidateOrder, etc.

Order Entity Class Example illustrating Entity’s Data and Behaviour
Order Entity Class

Domain services

Domain services are responsible for holding domain logic and business rules. All the business logic should be implemented as a part of domain services. Domain services are orchestrated by application services to serve business use-case. They are NOT typically CRUD services and are usually standalone services. Domain services are responsible for complex business rules like computing pricing and tax information when processing order, Order repository interface for saving and updating order, Inventory Interface for updating information about items purchased, etc.

It consists of algorithms that are essential to its purpose and implement the use cases that are the heart of the application.

Application services

Application services also referred to as “Use Cases”, are services responsible for just orchestrating steps for requests and should not have any business logic. Application Services interact with other services to fulfil the client’s request. Let’s consider the use case to create an order with a list of items. We first need to calculate the price including tax computation/discounts, etc., save order items and send order confirmation notification to the customer. Pricing computation should be part of the domain service, but orchestration involving pricing computation, checking availability, saving order and notifying users should be part of the application service. The application services can be only invoked by Infrastructure services.

Infrastructure services

Infrastructure services also referred to as Infrastructure adapters are the outermost layer in onion architecture. These services are responsible for interacting with the external world and do not solve any domain problem. These services just communicate with external resources and don’t have any logic. E.g. External notification Service, GRPC Server endpoint, Kafka event stream adapter, database adapters.

Observability services

Observability services are responsible for monitoring the application. These services help perform tasks like :

  • Data collection (metrics, logs, traces) — use mainly libraries/sidecars to collect various data during code execution.
  • Data storage — use tools that enable central storage of the collected data (sorting, indexing, etc.)
  • Visualisation — use tools that allow you to visualise the collected data.

Few examples include Splunk, ELK, Grafana, Graphite, Datadog.

Testing Strategy

Different layers of onion architecture have a different set of responsibilities and accordingly, there are different testing strategies. The testing pyramid is a great framework that lays out the different types of tests. Business rules that belong to the domain model, domain services and application services should be tested via Unit Testing. As we move to the outer layer, it makes more sense to have integration tests in infrastructure services. For our application End to End testing and BDD are the most appropriate testing strategies.

Concentric circles labeling testing practices by layer. Unit Testing for Domain Model, Domain Services and Application Services. Integration Testing for Infrastructure Services and End to End Testing for Application.
Testing Strategy For Different Layers

Microservices

Onion architecture is also applicable to microservices when viewing each microservice in isolation. Each microservice has its own model, its own use cases and defines its own external interfaces for retrieving or modifying the data. These interfaces can be implemented with an adapter that connects to another microservice by exposing HTTP Rest, GRPC, Thrift Endpoints, etc. It’s a good fit for microservices, where data access layer not only comprises database, but also for example an http client, to get data from another microservice, or even from an external system.

Application Structure & Layers

Application Structure & Layers covering how layers are mapped to modules and their dependency between each other. It also describes what testing strategy to be used for different layers.

Modularisation vs Packaging

There are two ways to organise application source code:

  • Either, we can have all the packages in a single module/project or
  • Divide the application into different modules/projects each responsible for a layer in onion architecture.

It greatly depends on the complexity of the application and the size of the project to divide source code into multiple modules. In a microservice architecture, modularisation may or may not make sense depending upon the complexity and use-case.

Frameworks, Clients and Drivers

The infrastructure layer composes frameworks for web or servers, clients for databases, queues or external services. It is responsible for configuring and stitching all the external services and frameworks together. Onion architecture provides decoupling so that it becomes easier to swap technologies at any point in time.

Do We Need Every Layer?

Organising our application in layers helps in achieving separation of concerns. But do we need all the layers? Maybe, maybe not. It depends on the use cases and the complexity of the application. It is also possible to create more layers of abstractions depending on application needs. E.g. for smaller applications that don’t have a lot of business logic, it might not make sense to have domain services. Regardless of layers, dependencies should always be from outer layers to inner layers.

Conclusion

Onion architecture might seem hard in beginning but is widely accepted in the industry. It is a powerful architecture and enables easy evolution of software. By separating the application into layers, the system becomes more testable, maintainable and portable. It helps easy adoption of new frameworks/technologies when old frameworks become obsolete. Similar to other architectural styles like Hexagonal, Layered, Clean Architecture, etc. it provides a solution for common problems.

--

--

Ritesh Kapoor
Expedia Group Technology

Software Architect — Passionate about Algorithms, Architectural Designs, Agile Methodologies