A guided tour of our service template
It happened the other day that a team I was working with felt the right answer to a question about which tech stack to use for a microservice was ‘Kotlin with Ktor using a ports and adapters architecture, building with as a multi-module Gradle project with the Kotlin Gradle DSL. Plus Guice for dependency injection.’
Only no-one on the internet seems to have implemented a microservice with that particular combination of structure and technology before. Or at least, no-one has talked about it. Each individual thing I mentioned, yes, there is some information available — but putting the pieces together was a challenge.
So… are you listening web-spiders? Are you paying attention, page-rank? I said:
- Kotlin with Ktor
- Ports and Adapters
- Gradle multi module project with the Kotlin DSL
Why these things?
Ports and Adapters: P&A, also known as ‘hexagonal architecture’, is an incredibly powerful mental tool for producing testable, clean architecture. I struggled for a long time to articulate why I found it so much more powerful than an ‘n-tiered architecture with DI’. I think it boils down to cementing the concept of ‘inside’ vs. ‘outside’. Business logic and the domain model goes ‘inside’. Everything else is outside. I have no doubt with sufficient discipline you can accomplish the same thing with other architectural styles. But applying P&A gives you some strong conventions to help guide your thoughts.
Gradle: Curiosity, mostly. I’ve used Maven extensively in the past and made my peace with it. But I’ve heard good things about Gradle and I was curious to use it ‘in anger’, as it were.
The Gradle Kotlin DSL: Because Groovy and I have parted ways. Also, curiosity. Also, masochism.
Multi-modules: Ahah. Well, now… I find the reaction to adding modules to a project, especially a relatively trivial microservice, quite mixed. Everything from, “It’s fine — the structure helps”, to “GET YOUR FILTHY STINKING MODULES AWAY FROM MY CODE.” (Well, the actual words were ‘What? No!’ — but my super-power is hearing what isn’t said.) It’s a surprisingly divisive issue! The reason I choose multi-modules is to manage dependencies. It helps keep you honest, helps you enforce your own rules. For example, ‘Domain shouldn’t know anything about JSON because it’s a transport layer concern’ or ‘Controller can’t talk directly to the database’. Sure, it adds some complexity and might feel unjustified for a small service. But if you’re producing multiple services you only take that complexity hit once — then you have a simple template to follow for all the rest. And, be honest, how often do your ‘micro’ services actually stay micro?
Guice: Because it’s not Spring, we’re used to using Guice with Play, and there was some assistance on the ktor website.
The code is the truth. You can find our ‘hello world’ service template here:
Let’s do a quick walk-through of the different modules though, their dependencies and any challenges involved setting them up.
Our ports and adapters implementation has four modules: App, Domain, Ports, Adapters.
Both ‘Domain’ and ‘Adapters’ depend on ‘Ports’. ‘Domain’ and ‘Adapters’ cannot see each other. ‘App’ can see all the modules.
What Goes Where?
The centre of the hexagon. Contains core business logic and the domain model. Importantly, contains no references to transport concerns like JSON, specific persistence technologies or time. Yes, time.
DateTime.now() is an anti-pattern at the domain layer because it complicates testing and creates a degree of temporal coupling. Pass in dates and timestamps from the adapter layer if you need them.
Everything outside the hexagon. Nothing related to business logic. Will contain things like JSON transformers, REST end-points, message handlers, event publishers, database repositories, scheduled events, and so forth
The ports module contains interfaces and DTOs (which will be Kotlin data classes in our case). It should have no actual logic and therefore not really need any tests. There are varying ways people arrange and name their ports, but the structure I like is to subdivide into two categories: Required and Provided.
Required ports are required by the ‘domain’ for the application to function. Hence they are implemented by the adapter module. Repositories, event publishers and API clients might be accessed through ‘required’ ports.
Provided ports are provided by ‘domain’ for use by the adapter layer. Hence they are implemented by the domain module. Service classes called from your controllers, event handlers and scheduled tasks are the most typical examples of ‘provided’ ports.
The ‘app’ module binds everything together and configures the framework being used to run the service. In this case it loads Guice modules and configures Ktor.
The guided tour:
There’s not much in our toy domain. Just a
SimpleGreeter that implements our
Greeter interface (a ‘provided’ port) and is injected with a
GreetingsRepository (a ‘required’ port).
Testing this is easy, we can just mock the repository. In general when testing ‘domain’ we want to mock out the ‘adapter’ implementations. As an aside, I prefer using kotlintest assertions with JUnit5 and Mockito-kotlin.
The most interesting thing about the adapter layer is how to ‘inject’ domain services into Ktor routes. The first step is to assume we can inject our Ktor
Application along with any ‘provided’ services and write a simple class that sets up the routes.
You can see above that once we inject the application class you can initiate the ktor routing DSL as per normal. We can also have multiple classes setting up routing. Effectively this lets our ‘route’ classes play the same role a Controller would in, say, a SpringMVC application.
The next step is to setup the rest of ktor, such as exception handling and content transformers. We do this in
The final step is to create a Guice module that binds the application for injecting, then feeds it into the route and config classes. The key here is to use
asEagerSingleton when binding the configuration and the routes. As the Javadoc says, “Useful for application initialization logic”
Writing adapter tests
This is where things get interesting. When we write ‘domain’ tests we don’t need to worry about much plumbing or wiring. Adapter tests, however, are where you probably want to write some integration tests. Given our hexagonal architecture, we’re looking for a way to start a test Ktor application but wire in mocks for all the domain services.
In our toy example we don’t have a ‘real’ database, so we can write integration-like tests to demonstrate the concepts without actually needing to integrate Docker or similar to provide external dependencies.
Step 1: Create mocks for our core services
There’s only one in our template, but it illustrates the point.
Step 2: Create a Guice injector
On line 3 above we inject the real Adapter Module. This means we can test the actual-factual routes with confidence. We also inject our mocked domain services.
MainModule here lets us inject our test application into the adapter module.
Also note that the
module function on line 2 above is an extension function off Application.
Step 3: Setup/TearDown methods to create a test application
Line 6 above we use the Ktor testkit to create a test environment.
Line 10 wires up the test application with our routes and mocks by calling the extension method from Step 2.
Line 12 injects the test class itself with anything in the Guice context so that we can wire mocks up into our tests, as we shall see in the next step.
Step 4: Write a test
In this step we subclass our test base to call a ktor route.
Worth calling out here is that on lines 4/5 we inject the mocked domain service into our test class.
Line 17 demonstrates a simple way to test a ktor route.
This adapter integration test gives us confidence that:
- The route works
- The service was called
- The HTTP call returned the expected status and content (so, for example, we could check JSON marshaling and unmarshaling was working)
Wiring up the application
Our ‘app’ module can see both ‘domain’ and ‘adapters’ and is responsible for wiring everything up and launching the application. There’s not much in here except for the actual main module and an entry point.
However — if you feel compelled to write a true end-to-end integration test against a real running service — this is the module to do it in.
I’m not sure if you should, though. If you trust the framework, and trust that your ‘layer crossing tests’ give you sufficient coverage that the adapters are calling the domain the right way and vice-versa, you can get away without needing to write any of these tests.
On the other hand, one or two ‘sanity tests’ that execute as part of an integration test phase probably wouldn’t hurt too much. You can find an example in the GitHub repository, but there is not much to learn from reviewing a code-snippet here. It is very similar in appearance to the Adapter test. Just less setup, because everything is real.
And all the rest …
Well that’s end of our tour! There is a little more to see if you care to explore the repository. You can see examples of how we managed to hack together a multi-module project with the Kotlin DSL. You can also see how we integrated some simple JWT authentication into our end-points.
Now I realize that the amount of setup code and Gradle configuration drastically dwarfs the amount of actual application code in this toy application. This may understandably lead you feel that it’s all more hassle than it’s worth.
I take your point, and I agree. But my counter-argument is that ports and adapters as an architectural style, as a discipline, really didn’t click for me until I’d built a couple of services with it and seen how cleanly, how beautifully, everything fell together. Please do give it a go, even if you are not feeling totally convinced.
If you happen to be using Kotlin and ktor, I hope this tutorial is of direct benefit! Most of this was figured out from scratch with little detailed knowledge of ktor or Kotlin. There might be better ways to achieve the same goals! Discussions and pull-requests against the service template in GitHub are welcome. If you are not currently using Kotlin, I hope it at least gives some insight into how you might organize your chosen technology stack into a ports and adapters structure.