After seeing Cyril Martraire’s online talk (in French) on Parleys about “Hexagonal” architecture and having read Robert C. Martin’s take on the “Clean Architecture”, I wanted to try it out for myself. It sounded great in theory, but reading a high-level description of an approach doesn’t teach as much as being confronted by numerous low-level implementation choices that result from the choice of high-level approach. I set up a little (but growing) personal project, and got going.
Following Cyril Martraire’s simplified approach, packages are either domain or infrastructure. The dependencies go only from infrastructure to domain.
The domain contains client-facing service objects (ex: AccountsService) and interfaces for low-level (but domain-focused) services used by them (ex: IAccountsStorage, ISecurity, IEventListener, IEventPublisherClient). It also contains value objects (ex: UserAccount).
Infrastructure contains everything else (ex: concrete implementations of the low-level domain-focused services, HTTP front ends, command-line controllers). My little project, in Java, relied on PostgreSQL for persistent storage, Redis for short-term session storage, and JBoss Wildfly for websocket and REST endpoints, a JMS queue, etc.
– CRUD happens with a call to a service object (ex: createUserAccount() in an AccountsService object), which then relies on the infrastructure implementation of the service’s storage interface (ex: PostgresAccountsStorage which implements IAccountsStorage) and other low-level interfaces (ex: BCryptSecurity implements ISecurity).
To unit-test the service, it’s best to stub and mock the low-level interfaces and verify validation of inputs, error handling and correctness of the call to the storage interface (for example, was the password which was passed to IAccountsStorage.createUserAccount() transformed by ISecurity? Note that you must not test the real encryption here because it’s a domain-level test – use stubbing, such as Mockito.when()).
In the infrastructure, it’s useful to refactor the storage tests to put all tests for the interface in an abstract class (in the domain, not in infrastructure), and then the concrete implementation of the tests just implements setUp() to provide the class under test. For example PostgresAccountsStorageTest just provides a real Postgres connection in setUp() – if I decide later to switch to MongoDB, I just implement a new setUp() with a real MongoDB connection in MongoAccountsStorageTest and I have all the failing tests already written in the superclass, AbstractAccountsStorageTest. Note that the tests in the AbstractXXXStorageTest classes are the only ones which actually access a real database.
Addendum: The pattern I describe for CRUD here seems to fit nicely with J.B. Rainsberger’s notion of contract tests and collaboration tests. The service tests are what he calls collaboration tests (where services are mocked) and then the tests which actually touch the database are what he calls contract tests, because they prove that the service really does what is asked. I like this terminology better than “unit” and “integration” tests, because the names reflect their function in verifying the implementation rather than just a notion without nuance of their scope.
– I also found it useful to create a session controller class in the infrastructure to act as an intermediary between the server endpoints (websocket, REST, etc.) and the domain service classes (more on this later…).
Difficulties and limitations:
- Logging – Is it a domain element or an infrastructure element? Risk of a multiplicity of logging solutions because some infrastructure elements will have their own logging dependencies (ex: log4j). I chose to have a domain-level logging interface implemented using slf4j-logback in the infrastructure. This may be over-engineering, since slf4j is already a facade.
- Extracting a low-level component – Tricky to try and pull out an infrastructure unit developed to make it a general-purpose stand-alone library because the infrastructure depends on the domain (including the domain-level logging interface – difficult to extract).
- Domain classes can’t contain Java-EE annotations and the like. Auto-wiring CDI dependency injection can get a bit complicated and may require wrapper classes or somewhat complex Producer implementations. Java EE was not designed with a clean/hexagonal architecture in mind.
- There are some gray areas for design. In particular, the session controller is responsible for calling domain services in a certain way and enforcing a certain flow of control. Flow of control seems like infrastructure to me, whereas the “certain way” of calling domain services (ex: the interpretation of a text command) seems like domain code. When in doubt, it’s infrastructure (the domain stays pure), but the need for some refactoring could emerge from this ambiguity. I will probably end up refactoring out infrastructure dependencies to put the control code in the second of 3 concentric circles (dependent on the domain but not the infrastructure) – which is closer to what Robert C. Martin proposed (though his diagram would put my session controller in the same circle with my PostgreSQL storage implementations, for example). This additional “control” circle seems to correspond to the ports layer described in this InfoQ article.
- Easy to swap out or offer alternative infrastructure elements (ex : change from Postgres to Mongo) without modifying the domain code at all and possibly without the need to write new tests.
- Domain code is easier to write, read, update and test because it’s unpolluted by infrastructure. Coding the infrastructure is often easier, too, because the interface with the domain is clear and clean.
- Can delay decisions about infrastructure and presentation, making progress on the domain logic.
- Easier to port domain code to other projects and/or infrastructures.
- Infrastructure development is driven by the domain use cases (YAGNI is easier to enforce)
- Facilitates IoC and DI, which makes the code easier to test, which makes the code easier to refactor, which makes the code cleaner, which makes changes easier to implement.
- Can’t lean heavily on a framework (ex: CDI, ORM, MVC, AOP, JMS/MDB), especially in the domain (where no framework dependencies are allowed). Leaning on frameworks can be useful for enforcing canonicality (having things defined in only one place – for example: strictly speaking, none of your domain classes should be generated by or have dependencies on an ORM – not even JPA annotations – so in a strict clean architecture approach you can’t use an ORM to generate your domain classes from database tables or vice-versa).
- Can’t easily optimize the domain code for specific infrastructures.
- Initial progress can be slow (as is the case with TDD in general).
- Domain logic changes can ripple out into multiple infrastructure adapter implementations.
- Complexity and redundancy in TDD, because use cases often need to be unit-tested and developed in multiple layers (ex: servlet, controller, service, and storage). If the end-to-end testing cycle is slow (heavy app server restart, WAR/EAR redeployment, etc.), forgetting to add a test at one of these levels can cost a lot of time.
Some thoughts about microservices:
The hexagonal/clean architecture is not the same as a microservices architecture, but the two are not completely incompatible. In a hexagonal/clean architecture, the goal is to separate the domain from the infrastructure. In a microservices architecture, the goal is to split the domain into small manageable pieces which communicate and inter-operate via a messaging infrastructure (REST, SOAP, JMS, ESB, whatever). According to Martin Fowler, you can’t start a development project using microservices. The domain has to be sufficiently complex (and useful/lucrative) to justify the effort and heavy tooling involved in managing a microservices approach, and the domain split has to be clearly defined. So a hexagonal/clean architecture might be the right place to start before evolving toward microservices. Because in the hexagonal/clean approach the domain is cleanly separated from infrastructure, splitting up the domain should be easier. Each microservice will start with a clean piece of the domain with interfaces to adapt to whatever infrastructure is needed for the specific microservice. It may even be possible to port much of the existing infrastructure code to the microservice, or to replace it with lighter infrastructure.
The hexagonal/clean architecture makes it easy to have and maintain quality code in the domain layer. It makes it easier to port the domain code to different infrastructures. The model/CRUD side of infrastructure is clearly and cleanly driven by the needs of the domain. The view/presentation infrastructure relies on well-defined service API’s to interact with the domain. These clear divisions of responsibility remove some sources of accidental complexity.
The approach seems to be a good one for most kinds of projects. However, for very small projects, for throwaway prototypes and short-term projects, and for projects requiring painstaking optimization for a specific platform, it might not be the best choice, but for all others, it seems like a very good choice for developing well-crafted, maintainable software which can evolve as infrastructure needs and possibilities change.