How to Write a Service
Generate the project
Section titled “Generate the project”Start from start.spring.io. Choose a current Spring Boot release — the generator only offers versions that are still supported, so take what it defaults to rather than pinning an older one — and a Java LTS release.
Set the coordinates to match the package naming convention, so the generated package and the published artefact agree from the start.
For an event-driven service the usual dependency set is:
- Spring Web — the inbound REST adapter
- Spring for Apache Kafka and Spring Cloud Stream — the inbound listener and outbound publisher
- Spring Data JPA and a driver — the outbound database adapter
- Lombok — to keep entity and DTO classes readable
- Testcontainers — so the adapters are tested against a real broker and a real database rather than mocks
Lay the source out along hexagonal lines
Section titled “Lay the source out along hexagonal lines”The point of the layout is that business logic has no compile-time knowledge of how it is reached or where its data is kept. Everything the service talks to sits behind a port — an interface owned by the business logic — with an adapter on the far side implementing it.
Inbound adapters convert something arriving from outside into a call on the business logic: a REST controller for synchronous requests from a user interface or another service, and a message listener for events consumed from the broker.
The business logic sits in the middle and depends on nothing but its own ports. It accepts and returns the service’s own types, never a framework request object and never a persistence entity.
Outbound adapters implement the ports: a repository that writes to the database, a publisher that writes events back to the broker, and an API client for any external system that has to be called synchronously.
Separate the write path from the read path. A command changes state and returns nothing beyond an acknowledgement; a query returns data and changes nothing. Keeping the two apart means the read side can be served from a projection shaped for reading, and that a caller can tell from the signature alone whether an operation has side effects.
Keep genuinely reusable, domain-free code — formatting, maths, document generation — in a separate shared library rather than in the service, so that the service’s own source contains only its domain and its adapters.
Why it is worth the extra interfaces
Section titled “Why it is worth the extra interfaces”- Business logic and adapters can be tested separately: the logic against in-memory fakes with no broker or database running, the adapters against real infrastructure.
- A transport or a datastore can be swapped by writing one new adapter, without touching the logic.
- A second inbound channel — adding a message listener to a service that previously only had a REST API — is additive rather than a rewrite.
See Event-based Microservices for how these services communicate once they are running.