At this year’s GOTO conference in Copenhagen, I attended the Hexagonal Architecture workshop held by Alistair Cockburn – the creator of hexagonal architecture himself. The workshop was top notch, as one would expect from Alistair, and at the very end of it we got an assignment to give LLMs a go at hexagonal style. So I thought I’d share my experience, and see if Claude CLI has the ins and outs of this particular architectural style.
TL;DR Claude gets the core parts right without any help, but for best results you’ll probably want to give it some instructions in CLAUDE.md
How Claude handles hexagonal architecture
We’re not going to go into the core tenets, what are the benefits, the costs, etc. There’s plenty of such material online, both from the author and others that have implemented the architecture within their environments. The focus here is on how Claude is handling it.
The app
We’ll be creating a very small note taking app with Java and Spring Boot, our favorite enterprise application development stack. The complexity is deliberately low, to see if Claude has the basics sorted.
The app enables the user to create, update and delete notes. A note will be simple text, nothing fancy. It will provide multiple ways to invoke it, via CLI and REST, and it will provide multiple ways of storing the note – in memory, the file system and the database.
My Expectations
If I were to give the task to a developer well acquainted with the hexagonal style architecture, I’d expect something like this:
- At least one driving port (interface) with methods for creating, updating, deleting and fetching single and all notes. I wouldn’t mind if there’s a Port for every operation, since each and every one can be mapped to a use case. That’s a good separation of concerns right there in the interface, which can aid readability since you can include the use case name right in the file name. However, it’s not mandatory in any way.
- At least one driven Port (interface) for note persistence. This I expect to be a Spring Data JPA repository, named something like NoteRepository, of course in interface form.
- 3 implementations of the NoteRepository interface, one for in-memory, one for file and one for DB based persistence.
- 1 implementation of the driving port. At this stage I’d expect to be just one implementation class implementing all one or multiple interfaces, however many of them there are.
- 2 adapters, one in REST @Controller form, the other one in @ShellComponent form (from Spring Shell project)
How does Claude perform?
Let’s take it for a spin without any help. There’s plenty of hexagonal architecture materials available online for a long time now, so Claude should be familiar with the style.
Without CLAUDE.md
The first prompt I fired was: “Create a note taking app, with memory and file system based storage, accessible through cli and rest, using spring boot framework”
The result? Let’s take a look.
Folder structure:
└── wearenotch
└── notes
├── application
│ └── NoteService.java
├── domain
│ ├── Note.java
│ └── NoteRepository.java
├── infrastructure
│ ├── adapter
│ │ ├── cli
│ │ │ └── NoteCommands.java
│ │ ├── persistence
│ │ │ ├── FileSystemNoteRepository.java
│ │ │ └── InMemoryNoteRepository.java
│ │ └── rest
│ │ └── NoteController.java
│ └── config
│ └── ApplicationConfig.java
└── NotesApplication.java
Okay, not bad. We have a NoteRepository which is a part of the domain (the hexagon), and the folder structure reflects that. Good. The infrastructure folder is next to the application and domain folders implying that there’s no ownership of one over the others. Also good. What’s missing here is the notion of which adapters are for the driving and driven ports.
No driver port in sight
The NoteService isn’t implementing any interfaces. Although not necessarily a problem it makes the boundary of the hexagon less visible and less explicit than it could’ve been. It also makes potential future refactoring a bit more difficult if we decide at a later stage to slice the implementation across use case boundaries. So, not a deal breaker, but still.
Driven port in place
The most important part of the hexagon is done right. Well done, Claude!
Configuration
Configuration is all done via @Component, @Repository, @Controller and other annotations, making the application less configurable if it were done via a dedicated @Configuration class being driven by values in application.properties. This also made the app instantiate all driven adapters when only one is in use. Which brings me to my next prompt.
Configurable configuration
Prompt: “Update the code so that the beans are instantiated and configured in a dedicated spring configuration class”
Claude happily changed the code and started relying on @ConditionalOnProperty annotations. It also removed framework annotations from the NoteService, the core hexagon class.
Creating the tests
During the previous two prompts Claude didn’t create any tests. Which is okay I guess from a prompting perspective, but fully unacceptable if you’re a developer working with hexagonal architecture. So let’s prompt Claude to create them.
Prompt: “create the tests please”
The result is displayed below:
└── wearenotch
└── notes
├── application
│ └── NoteServiceTest.java
├── domain
│ └── NoteTest.java
└── infrastructure
└── adapter
├── cli
│ └── NoteCommandsTest.java
├── persistence
│ ├── FileSystemNoteRepositoryTest.java
│ └── InMemoryNoteRepositoryTest.java
└── rest
└── NoteControllerTest.java
Aiming 100% code coverage Claude created tests for literally everything, including the Note domain class. Uh-oh… Since this class is just a data class the test is not justified. Also, the NoteControllerTest and NoteCommandsTest I wouldn’t do personally, because they don’t bring much value – if any. They are just thin access layers that would be best tested indirectly through an integration test. Speaking of which, the integration test is nonexistent, but that’s just a prompt away so I won’t hold it against Claude.
The verdict
Not bad, all things considered. Claude did the folder structure and the driven port interfacing right. However, it can do much better, so let’s give it some help.
With CLAUDE.md
Based on the attempt without help, I’ve created CLAUDE.md with the following content.
“`
This is a hexagonal architecture styled project.
The folder structure in the main package in src/main/java is the following
/app
/app/service
/app/port/driving
/app/port/driven
/adapter
/adapter/driving
/adapter/driven
/config
/domain
Every use case should be represented as an interface in the /app/port/driving folder.
Use case implementations should be in the /app/service folder.
Make sure, at all costs, not to leak any framework or library classes details into the port and service folders.
“`
You can see there’s not much help inside. Other than the folder structure – which is not strictly prescribed by Alistar – it’s just a short rehash of the main tenets of the hexagonal architecture style.
The prompt I wrote: “Create a note taking app, with memory and file system based storage, accessible through cli and rest, using spring boot framework”. I’ll admit, it wasn’t the product of much thought, which actually makes the whole thing even better. Now let’s take a look at the code.
The Code
- It followed the folder structure described in CLAUDE.md to a T. Well done, Claude! There are reasons why I like this folder structure and naming, but let’s continue looking at what Claude did.
- It created a driving port (interface) for every use case, and even named them CreateNoteUseCase, UpdateNoteUseCase, etc. Nice! Now if you want to see what the app is all about, the app/port/driving package is the place to start exploring the code. And if you know hexagonal architecture you don’t even need me to tell you that :)
- Moving on, it created one driven port, the NoteRepository interface in the app/port/driven package. All correct.
- All these interfaces are without any Spring or REST or CLI stuff polution. They know nothing about what the adapters are going to do, as is the way to go. More points for Claude.
Implementations and adapters.
- NoteService is implementing all driving ports, that’s perfectly fine.
- Only the File system backed NoteRepository adapter is fully implemented. However, a simple prompt away (“you forgot the memory based storage”), and we have the memory backed InMemoryNoteRepository. It also added configuration entries for choosing which style the app is started with.
- The REST and CLI based driving adapters are also fine, in their respective folders. They have Spring specific annotations in them, as one would expect. This is where the interface meets the implementation technology after all.
- The configuration class, ApplicationConfiguration, is perfectly laid out, Spring conventions are followed, nothing to change here.
- The Note domain class is tad richer than I thought it would be, but on the positive side. ID, Title, Content, CreatedAt, UpdatedAt are all welcome fields.
Tests
This is where I threw Claude a bit of a curveball. My prompt was just “create the tests please”, and here’s where it stuttered a bit.
It created the Controller test with mocked service, and although I’d rather see a full blown integration test with the entire app up and running Claude just tested the controller. One would be able to argue that it tested the adapter only, in a unit test fashion, but I think in this case it’s just pointless. The integration test was what I wanted. Same with the CLI based adapter test.
NoteServiceTest is just the way it needs to be, Mockito test with mocked repository. Repository adapters are also tested, and the file based is ofcourse flaky because it deals with the file system.
Interface party
At this point I decided to spice things up even further. I told Claude “Add the relational DB backed note storage. Use postgres” and things got interesting.
Besides configuring the app to connect to Postgres and creating a JpaNoteRepository it decided not to do interface extension like this:
interface JpaNoteRepository extends JpaRepository<Note, Integer>, NoteRepository
This would lead to annotating the Note domain class, the exact one that shouldn’t be polluted with framework and any other stuff, annotations included.
It instead decided to create a JpaNodeRepository (simple spring data jpa based repository), and wrap it into a DatabaseNoteRepository, where it decided to do the domain<->JPA object mapping, using the fromDomain and toDomain methods, that it created.
interface JpaNoteRepository extends JpaRepository<Note, Integer>, NoteRepository
This would lead to annotating the Note domain class, the exact one that shouldn’t be polluted with framework and any other stuff, annotations included.
It instead decided to create a JpaNodeRepository (simple spring data jpa based repository), and wrap it into a DatabaseNoteRepository, where it decided to do the domain<->JPA object mapping, using the fromDomain and toDomain methods, that it created.
public interface JpaNoteRepository extends JpaRepository<NoteEntity, String> {}
public class DatabaseNoteRepository implements NoteRepository {
private final JpaNoteRepository jpaNoteRepository;
public DatabaseNoteRepository(JpaNoteRepository jpaNoteRepository) {
this.jpaNoteRepository = jpaNoteRepository;
}
@Override
public Note save(Note note) {
NoteEntity entity = NoteEntity.fromDomain(note);
NoteEntity savedEntity = jpaNoteRepository.save(entity);
return savedEntity.toDomain();
}
}
The potentialy ugly parts
Lazy and eager loading
A good architect will immediately spot the lazy/eager collection problem. In Spring apps it’s common to work with JPA entities throughout the service layer, and lazy loading can bring significant speedups when queries become complex and your tables contain hundreds of thousands or millions of rows. With hexagonal it’s all mapped at once. Yikes :(
However, it’s also usual – and well supported by Spring – to load only selected data by means of creating special smaller models for a specific use case. And yes, these use cases map directly to interfaces talked about previously that Claude happily generates, e.g. CreateNoteUseCase, DeleteNoteUseCase etc.
Transaction handling
When using Spring, the transaction handling is by far the most frequently done with @Transactional annotation. The convenience is simply unbeatable. And in an architecture as above it’s best placed on a public service class, like this:
public class NoteService implements CreateNoteUseCase, ... {
@Override
@Transactional
public Note createNote(String title, String content) {
...
}
}
And there we have it, we have dirtied the hexagon with a framework class. The solution to this is not easy or nice. For the transaction proxy to kick in, it’s required to place it on a public method of a class – that is invoked by another class – not by itself, or you have to do some really strange proxied loops. And you don’t want to go there, trust me, it’s ugly as hell ;). Here, it means on @Controller or @ShellCommands classes. And that is simply not right. Let’s explain:
- Why would an adapter class know anything about how the service logic keeps its act together? Talking about a leaky abstraction…
- If you place it on the Controller, you’re supposed to place it on a ShellCommands class as well, and this is maintenance hell, a system that depends on you repeating yourself. Pure madness.
The least possible evil here is to leave @Transactional it in the service class. Yes, in prod you’re introducing a transaction that won’t be there in the unit tests, but the code will behave the exact same way in both scenarios. And in the unit tests you don’t care about the rollback of all transactional resources, it’s just the repository errors that matter, and those should be simulated in the unit tests. Not because of the transaction concept, but because external systems can and will be unavailable and malfunction in this or that way and your service layer should account for that.
Conclusion
Hexagonal as an architectural style wasn’t made for Spring. It predated Spring by a couple of years, and it wasn’t as refined and popular when Spring came out. However the two of them can play along nicely if you’re willing to squint a bit.
When it comes to Claude, it knows it very well. Claude creates the ports and adapters as the style demands it, and readily accepts help in terms of e.g. folder names etc. in CLAUDE.md. In my books, it doesn’t pass with flying colors because of the test hiccups. You also might not like how the JPA entities are mapped to the domain model and the transaction part, but Claude is not to blame here :)
Although not outstanding, Claude gets a very good score from me.