The Trinity Architecture
The Trinity Architecture proposed here is an architectural pattern for backend enterprise applications. It emanates from a typical 4-layers architecture employing the Dependency Inversion Principle (DIP). It is ideal for, but not limited to, Domain-Driven Design (DDD) applications.
The three pillars of Trinity are:
- the Domain Model (DOMAIN)
- the public Application Programming Interface (API)
- the Auxiliary Services (AUX)
Trinity emphasizes on balancing uncontrolled flexibility with consistency. It provides specific implementation guidelines with eight top modules.
The dependencies rules are:
Circles inside a circle: Outer circles depend on the inner ones.
Tangent circles: Outer circles use (clients) or implement (details) the inner ones.
2. A brief overview of popular architectural patterns
In the beginning, when computer science appeared in the 50's/60’s, there was no architecture as we know it today. Developers were solving problems by optimizing algorithms and data structures.
The term Software Architecture became prevalent only at the beginning of the 1990s. The reason was the rise of client-server systems. With client-server systems, the pattern of multi-tier architectures appeared.
Later, in the same decade, the layered architectures emerged. The layered ones break down a system into logical layers rather than physical.
In the 2000s new architectural patterns appeared, based on the Dependency Inversion Principle. Their main goal was to solve the tight coupling problems of layered architectures.
2.1 The Multi-Layered Architecture
The most popular architecture of all times is the multi-layered one. The pattern does not specify the number of layers, but a simple rule:
A layer may couple with any layer below it, but never above it.
Many teams may argue about the suitable number of layers in a layered architecture.
Martin Fowler (2002) suggests three layers. (1) Presentation, (2) Domain, and (3) Data Source.
Eric Evans (2003) suggests four layers. (1) User Interface, (2) Application, (3) Domain, and (4) Infrastructure.
Benefits of Layered Architectures
- Separation of Concerns
Drawbacks of Layered Architectures
- Lack of inbuilt scalability
- Hidden use cases
- Business Logic may spread out from UI to Database
- Cumbersome, if not prohibitive, to interchange frameworks and technologies
- Complex Testing Processes
- Risk of too many, unnecessary, layers
2.2 The Dependency Inversion Principle
Robert C. Martin formulated the Dependency Inversion Principle (DIP) in 1996. It stands for the letter “D” in “S.O.L.I.D.” principles. Today, it is one of the most famous principles in object-oriented programming.
The formal definition of DIP is:
A. High-level modules should not depend on low-level modules. Both should depend on abstractions (interfaces).
B. Abstractions should not depend on details. Details (classes) should depend on abstractions.
Vaughn Vernon (2013) demonstrates how to apply DIP in a typical 4-Layers Architecture.
With this simple reversal, the Domain Layer is the one that all the other layers depend on. This approach eliminates the direct coupling to the Infrastructure Layer.
2.3 DIP-based Architectures
After the mid-2000s a range of ideas appeared on the architecture of applications.
- Ports & Adapters (Hexagonal) by Alistair Cockburn, 2005
- Onion by Jeffrey Palermo, 2008
- Clean Architecture by Robert C. Martin, 2012 / 2017
Although they vary in their details, their goal is the same. They all aim for better Separation of Concerns through DIP. We could call this family of architectural patterns Concentric.
According to Robert C. Martin, the dependency rule behind them is:
Source code dependencies must point inward, toward higher-level policies.
Benefits of DIP-Based Architectures
- Independent of the UI
- Independent of the database and other external systems
- Independent of technology-specific libraries and frameworks
- Interchangeable infrastructure modules
- An arbitrary number and types of clients
Drawbacks of DIP-Based Architectures
- Lack of templates (except Onion)
- Vague terminology from previous decades
- Vulnerable to erroneous dependencies and modules structure (if not done right)
- How to choose a pattern?
3. Formation of Trinity
The goal of Trinity is to solve the drawbacks of DIP-based architectures. In this section, its formation will take place. The required steps are:
- Start from Typical 4-Layers and define Generic vs Specific Layers
- Define Strict and Relaxed Dependencies
- Apply DIP
- Include Application Module and form the Trinity Architecture
3.1 Define Generic & Specific Layers
The starting point of Trinity is the typical 4-layers, as proposed by Evans in 2003. At that time, technologies like REST were unknown to the IT community. We will now adjust the terminology to the latest standards.
User Interface Layer -> Clients
Applications today are not limited to a single user interface client. Instead, there can be many Clients. It is noteworthy the plural of the noun Client as there can exist many of them (i.e. REST client, Web MVC client, etc.)
Application Layer -> (Public) API
The application layer per se does not distinguish between public and internal services. Here, we will enforce it to be a specific layer denoting its purpose by renaming it to (Public) API.
Domain Layer -> Domain
The Domain layer, being the core of an application is already a very specific layer. So, it remains as it is except for removing the word layer.
Infrastructure Layer -> Auxiliary
The term infrastructure may be confusing, especially for junior developers. One may think about physical servers only, such as a database or messaging servers. This is not the only case though. Infrastructure is also a service implementing a complex encryption algorithm, for example. Thus, we replace the term infrastructure by the term Auxiliary. Auxiliary describes better anything that helps to the purpose of an application.
3.2 Define Strict and Relaxed dependencies
In layered architectures there are two types of coupling between the layers:
- Strict: allows coupling only to the one layer below it.
- Relaxed: any higher level layer can couple to any layer below it.
Clients -> API Clients
Following the best practices of DDD, public API is the only direct client of the domain layer. Thus, clients are not allowed to couple to the domain. Here, they are not allowed to couple to the auxiliary layer either. As a result, clients have Strict coupling only to the API. We also change the term Clients to API Clients to denote the strict coupling to the API layer only.
(Public) API -> API
The API should always be public, so we remove the redundant prefix (Public). It has relaxed dependencies to both Domain and Auxiliary layers.
Domain -> DOMAIN
In this phase, the Domain layer remains as it is. This means it depends on the Auxiliary layer only. Its name becomes DOMAIN.
Auxiliary -> AUX
The Auxiliary layer has no dependencies and its name becomes AUX.
3.3 Apply DIP
In the next step, we apply the DIP to all the layers below the first one, the API Clients. This is the most critical step.
- The API defines DTOs and abstractions only.
- The DOMAIN defines models and all abstractions it needs. Thus, we remove the coupling to the AUX layer.
- The AUX defines DTOs and abstractions required for the application.
- The API Detail implements the API abstractions using the DOMAIN and AUX abstractions.
- The DOMAIN Details implement the DOMAIN abstractions
- The AUX Details implement the AUX abstractions
- The API Clients use the API abstractions. Not the API Detail.
As the first three modules define abstractions only, they are frameworks agnostic.
3.4 Include application (APP) module and rotate
Next, we include The Application Module, which is necessary to bootstrap the application. It is also responsible for configuring the APP, crosscutting concerns, etc.
By rotating and re-organizing the blocks we can now form the Trinity Architecture.
The lower row is the Trinity Core being frameworks/technologies agnostic. The upper row is the Trinity Composition. It provides, implements, and uses the Trinity Core.
An alternative view is the circular one, depicted at the top of the article.
4. The Trinity Architecture
4.1 The Trinity Core
The three pillars of the Trinity Architecture are:
- Domain: The Domain Model
- API: The public Application Programming Interface
- AUX: The Auxiliary Services
Each one of these modules does not depend on any one of the rest. Their role is to define DTOs, Models, and Abstractions only. They also should not have any dependencies on frameworks related libraries. The only acceptable exception is the usage of frameworks specific annotations if desired.
4.2 API Detail
The API Detail is the glue of the three independent components API, AUX and DOMAIN.
Its role is to implement the API by using the Domain and the Auxiliary models and abstractions. The API Detail remains frameworks agnostic except for frameworks specific annotations.
4.3 API Clients
The architecture supports the addition of an arbitrary number of API Clients. The API Clients depend only on the API Abstraction and not on the Domain model or the Auxiliary Services. In this way, we ensure that there is always a single entry-point to the application for every use-case.
4.4 AUX Details
The AUX Details provide the implementations required for the application. You should separate the details into different modules according to their role. For example:
- Document Store
- SMS Service
- SMTP Service
4.5 Domain Details
The Domain Details provide the implementations required for the application. You should separate the details into different modules according to their role. For example:
- Domain Services
- Payment Gateway
- Template Engine
Bootstraps and configures the application.
5. Related Tools & Work
A demo of Trinity architecture for Java is available at Github.
The names of the modules are 1:1 to the ones used in Trinity Architecture diagrams.
Trinity Scaffolder for Java
It is possible to generate your own project in Java with the Trinity Scaffolder.
trinity4J is a set of Domain-Driven Design Libraries for Java Applications. It is a perfect fit for the Trinity Architecture.
The Trinity Architecture is part of a bigger project called PolyGenesis.
PolyGenesis is an open source platform for Cross-Language Automated Code Generation. It focuses on DDD and UI.
The Trinity Architecture characteristics are:
- Clear terminology & well-defined modules structure
- Frameworks independent core (API, AUX, DOMAIN)
- An arbitrary number of API Clients, Domain Details, and Auxiliary Details
- Interchangeable technologies & frameworks
- Better allocation of team members
- Low learning curve
- Supporting tools and libraries
The Hexagonal Ports-Adapters is one of the most prevalent architectures in the DDD community. Comparison of Trinity with Hexagonal Ports-Adapters is provided in this article: https://medium.com/oregor/comparison-of-trinity-architecture-to-hexagonal-ports-adapters-e49d21b0ba94