Why learn about Hexagonal Architecture?
If you are building a new system and are looking for a technique that makes it reslient to change, read on. If you are looking to make your automated testing easier, read on. If you are looking to make it easier on your team to know where components should live, read on.
What is it?
I've been developing software solutions on the JVM for quite some time and, along my journey, I've discovered a technique that has proven to be quite useful: Hexagonal Architecture. Also known as the Ports and Adapters architecture, I've experienced the benefits of the technique on several projects and would like to share my experiences with you.
Alistair Cockburn defines the technique like so:
Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases. As events arrive from the outside world at a port, a technology-specific adapter converts it into a usable procedure call or message and passes it to the application. The application is blissfully ignorant of the nature of the input device. When the application has something to send out, it sends it out through a port to an adapter, which creates the appropriate signals needed by the receiving technology (human or automated). The application has a semantically sound interaction with the adapters on all sides of it, without actually knowing the nature of the things on the other side of the adapters.
Layering your objects in such a way isolates your core logic, the pieces specific to your business, from elements you have less control over, such as integrations with external APIs. This isolation eases the burden of testing and changes in external APIs. One system I worked on was able to replace a legacy Flash communication layer with a more modern REST one, leaving the core code untouched. As with most decisions, there are trade-offs to weigh and I'll address them as we describe the individual components of the solution.
I've found that replacing the Adapter concept with Martin Fowler's Gateway makes it easier for developers to understand where in the layering scheme objects should go. In addition, replacing the hexagonal diagram with something a little more traditional also increases the understanding of the layering.
Implementation Examples
My experience with this technique has been with Spring on the JVM, so the samples will be presented in that context.
Terms
Before we can look at examples, we'll need to settle on some terms:
- model - the objects that the core components use to express the domain they operate in
- data transfer object (DTO) - objects that represent data entering and leaving the process. Only the gateways should be using DTOs.
- inbound gateway - objects that respond to requests coming into the process, such as REST controllers or AMQP message handlers
- outbound gateway - objects that manage interactions outside of the process on the behalf of core objects, such as accessing a database or sending a message to a logger
- service stub - a test double of an outbound port that is used to quicken and stabilize tests
Outbound Gateway
This section of the layering manages any contact outside of the process originated by the core, such as accessing the file system or network. The recipe is very simple. First, create an interface for the Port, expressing the services in terms of the core's model objects. Second, implement the interface, having the gateway object translate the core models into the particular DTOs and API calls required by the integration. Keeping all the specifics for accessing the external service internal to the gateway object is extremely important. If integration details are allowed to leak into the core layer, the benefit of the technique is lost.
interface TranslationPort {
TranslationModel translate( String text, Language from, Language to )
}
@OutboundGateway
class GoogleTranslationGateway implements TranslationPort {
TranslationModel translate( String text, Language from, Language to ) {
// call out to Google for translation, converting the results into the core model
def requestDTO = createRequest( text, from, to )
def result = restTemplate.get( reqeustDTO )
toModel( result )
}
}
This setup is simple but it does require a bit of discipline to make it work and should be a focus during code reviews. The core obects only iteract with the port interface, decoupling itself from the gory details of the Google Translate integration. The trade-off we make for this decoupling is the extra work required to copy data between the DTO and model.
The OutboundGateway annotation is a custom Spring stereotype that does a couple things. First, it marks the class as a Service, enabling Spring's classpath scanning to pick it up for auto wiring. Second, it places a Qualifier on the class of "production", which is necessary when Service Stubs are being used.
Tests involving outbound ports take two forms: service stub and production implementations. Leveraging Spring profiles, we can swap out the real gateway for a fake one during testing. The rule is that one set of tests exercise the real implementation while the remaining tests use the service stub. There are numerous benefits of using this model but stability and speed are the two primary reasons. Service stubs are normally implemeted using in-process structures making them immune to network instability and very fast. The production implementation, however, doesn't offer the same guarantees, which is why we limit the amount of tests against them.
When testing the production gateway, we need to let Spring know that we don't want the Service Stub but the real implementation instead. That is done via the Qualifier annotation. The example below is a Spock test that exercises the actual integration, using a JUnit Category to give us the ability to selectively include or exclude the outbound tests. This is a handy feature allowing develpers to work in disconnected environments, such as the train.
@Category( OutboundGatewayIntegrationTest )
@SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.NONE )
class GoogleTranslationGatewayIntegrationTest extends Specification {
@Autowired
@Qualifier( 'production' )
private TranslationPort service
def 'Test English to German Translation'() {
expect:
def model = service.translate( 'mother', Languages.ENGLISH, Languages.GERMAN )
model.translation == 'mutter'
}
}
To illustrate how the Service Stubs behave during integration tests, let's look at some code.
@ServiceStub
class GoogleTranslationServiceStub implements TranslationPort {
TranslationModel translate( String text, Language from, Language to ) {
// precautionary log message to help notice inadvertent stub activations
log.warn( 'Service Stub in play.' )
// return a hard coded phrase for the specified language
def result = lookupPhrase( to )
toModel( result )
}
}
Again, a custom Spring Sterotype is in play, giving Spring a couple of signals. First, it marks the object as a Service, allowing it to be detected during scanning for autowiring purposes. Second, it designates the object as the primary instance. Remember, when the tests are run there will be two implementations of the service interface and Spring will need guidance to know which one of the instances to wire up. Lastly, the object is only to be instantiated when the "use-stubs" Spring profile is activated. The code below illustrates how we might integration test our stub, which we would never do in a real project.
@Category( OutboundGatewayIntegrationTest )
@SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.NONE )
@ActiveProfiles( 'use-stubs' )
class ServiceStubGatewayIntegrationTest extends Specification {
@Autowired
private TranslationPort service
def 'Test English to German Translation'() {
expect:
def model = service.translate( 'mother', Languages.ENGLISH, Languages.GERMAN )
model.translation == 'mutter'
}
}
The above code looks very similar to the production test with a few small but important differences. The first one is the activation of the "use-stubs" Spring profile. Normally, you wouldn't need to set this in the test itself, instead allowing Gradle to activate it during the test phase of the build. The second difference is that lack of a Qualifier annotation on the service bean. Remember, the ServiceStub Sterotype designates the bean as the primary which means that despite the fact that two beans of the same type exist, production and stub, the stub instance will be selected by Spring and injected into any beans that specify the dependency.
Inbound Gateway
On the inbound side, at least in a Spring context, the relationship of Port and Adapter is inverted. The adapter handles incoming signals, such as an HTTP GET call, and translates the incoming DTO into a model before invoking a Port. Examples of inbound gateways include REST controllers and AMQP message listeners. Lets take a look at an example.
interface InventoryPort {
InventoryModel lookupItem( final UUID identifier )
}
@RestController
class InventoryGateway {
@Autowired
InventoryPort port
@GetMapping( path = '/inventory/{itemID}', produces = [MediaType.APPLICATION_JSON_VALUE] )
ResponseEntity<HypermediaControl> fetchItem( @PathVariable String itemID ) {
def uuid = toUUID( itemID )
def model = port.lookupItem( uuid )
def dto = toHypermediaControl( model )
new ResponseEntity<HypermediaControl>( dto, HttpStatus.OK )
}
}
Much like the outbound gateway, the inbound gateway manages HTTP interactions as well as the conversion between DTO and model. The real difference between the gateways is that the inbound variety calls into the core logic via the Port interface. As with the outbound gateway, we are trading isolation between layers for the overhead of translating between model ad DTO.
I purposely selected a simple query service as an example because it illustrates a common objection to the layering. The path used to read in the data is Inbound Gateway -> Core Service -> Outbound Gateway, which seems like an unecessarily indirect route to the answer. Once validation, access control, caching and orchestration logic is installed, developers come to realize the benefit of the layering. Even if the core service contains just the pass-thru code, the separation is worth it for the testing convenience alone. The inbound gateway can interact with mocks and service stubs as needed, simplifying testing. The scenario where a whole inbound protocol is replaced with another, such as my previous AMF to REST scenario, doesn't happen very often but changes to the inbound API are quite common. Adding API key support or rate limiting are "plumbing" issues and should be insulated from the core logic.
When integration testing on the inbound side, it often unnecessary to use Service Stubs because logic in the core is never supposed have direct access to outside the process, keeping them very fast and stable. What normally happens is that the outbound gateways are Service Stubs but the inbound gateways and core components are "real" and can be exercised using a near-prodution configuration.
Core Component
Of the 3 layers, the core is one that requires the most discipline to implement. As developers, we are accustomed to using what ever API we need to get the job done, often ignoring the maintenance consequences of our decision. As peers, we must remain vigilant during code reviews ensuring that core components never violate the "only leave the process via a gateway" rule. Sometimes, however, it isn't always obvious that a component is violating that rule. A common example of a subtle violation is the direct use of logging APIs. It is all too easy to forget that your SLF4J call actually manifests as a disk write or a network call. All out-of-process access must flow through a gateway.
interface InventoryPort {
InventoryModel lookupItem( UUID identifier )
}
interface StoragePort {
InventoryModel lookupItem( UUID identifier )
}
interface FeedbackProvider {
void sendFeedback( FeedbackContext context, Object... arguments )
void sendFeedback( FeedbackContext context, Throwable error )
}
interface FeedbackAware {
FeedbackProvider getFeedbackProvider()
void setFeedbackProvider( FeedbackProvider aProvider )
}
class ProductionInventoryService implements InventoryPort, FeedbackAware {
@Autowired
StoragePort storagePort
@Autowired
FeedbackProvider feedbackProvider
InventoryModel lookupItem( UUID identifier ) {
feedbackProvider.sendFeedback( INVENTORY_LOOKUP, identifier )
storagePort.lookupItem( identifier )
}
}
The implementation of the component is simple but illustrates that any call that leaves the process must go through a gateway, including logging. In practice, I've found that the ability to swap out the logging gateway for a test double can aid in testing functions that do not return a value. My experience is that many core services start out simple like the sample above but often become increasingly more complex over time. You can imagine adding caching to the lookup or access control, requiring additional code in the service.
The trade-off in this layer is the potential performance hit due to the indirect access to the APIs doing the actual work. Every project is different but I have yet to see the isolation benefits get outwayed by an extra call, especially when you factor in today's modern JVM and its languages.
Considerations
Does this work with microservices?
To be honest, my micoroservice-based projects haven't been in production long enough for me to make a determination whether or not the discipline required to maintain the layers is worth the benefit. My default, when starting a new service, is to use the Hexagonal layering. More times than not, it appears to be helpful due to the greater control over testing. Some services, however, are very small and you question whether having a single inbound gateway, a single core compoenent and a single outboud gateway makes sense. You have to decide for yourself if the service will be iterated on through its maintenance cycle or simply be replaced.
Can this aid in the transition from monolith to microservices?
The advice to craft new projects as monoliths makes sense to me, especially if you are working in "unknown territory". One programming language, one set of utility libraries, the ability to easily refactor the code base are all good reasons to start out with a monolith until the solution matures. What I am currently experimenting with is using the Hexagonal layering in a way that eases the transition to microservices. The idea is to partition the primary services into their own "hexagons" that can be later pulled out into microservices. The challenge is to isolate the different services from each other and maintain that isolation throughout the refactorings. Being in the same code base makes it way too easy to "jump the fence" and call a sibling service directly, breaking the isolation. My current idea is to implement inter-service gateways using ZeroMQ which is a messaging system that can operate both in-memory and distributed. In theory, you could move a previously in-process service into its own microservice with only minor changes to the ZeroMQ configration.
What package conventions should I use?
Some objects will cross layer boundries and need to live in a shared package while other objects should never be visible to other layers. Here is a package scheme that has worked for me on Groovy projects.
- inbound - controllers, messages listeners and DTOs live here
- outbound - gateways and DTOs live here
- core - service implementations live here
- shared - outbound port interfaces, core port interfaces, core models, application configuration object and shared message context live here
- spring - global Spring error handlers, interceptors and other Spring related objects go here
Should I use repositories directly?
Spring has implementations of the repository pattern, making it very convenient to iteract with data stores. The way it works is that you define an interface and Spring generates an implementation at runtime. You could argue that this pairing is the Port and Adapter model so layering concerns don't apply. In my experience, wrapping the repository with an outbound gateway is extremely useful. Database specific exception handling doesn't impact the core logic is one benefit. Another is that the core model and the persisted entity do not have to be the same object since the gateway can handle the translation between the two objects. Optomistic locking and version fields usually don't make sense in the core model and should be left out.
Summary
The Hexagonal Architecture has proven to be extremely useful to me in the context of various sized monolithic implementations. Simplifying testing is the primary benefit with the ability to swap out both inbound and outbound integration points a close second. The utility of the layering approach in a microservice context, however, is still an open question but my initial evaluation is that the testing benefits make it useful in almost any context.