Code Structure
Simple Services
Simple services refer to light-weight services that do not have much business logic in them. These could be CRUD utility services (e.g. merchants-manager which just tracks the merchants for each transaction type), or gateway services which act as a service layer for external APIs/dependencies.
Overview
A simple package structure should suffice for simple services, since a more complicated one just increases overhead and complexity unnecessarily.
Structure Description
ph.safibank.<projectname>.
controller
- for controllers serving REST APImodel
- containing model in form of Kotlin data classes.repository
- containing repository classes (interfaces in case of using Micronaut Data Repository interfaces)service
- services layer. This layer is written usually in form of couples of interfaces & implementation. It is convention introduced by Spring authors and de-facto standard in java world.utils
- containing for example function for easier logger instantiation or set of exceptions specific to microservice.
Complex Services
Overview
For complex services, we implement a hexagonal architecture to our services. We start from the domain/business logic and describe the actual capabilities the service has, as well as defining any dependencies it needs from other domains. The main idea is to drive the development based on the needs of our domain/core business logic, instead of being driven by the dependencies.
Structure Description
ph.safibank.<servicename>
core
- contains main business logic for service. Code here should have no dependency on any code residing inapi
/infra
, and should be the first part of the code actually done. Components are organized by domain (e.g. account, fraud, etc). Should dependency on any other top level packages except for utils.usecase
- contains use case interfaces and classes which define the actual capabilities of the service<domain-package>
- contains interfaces and models expected for specific domain (e.g.account
,provider
, etc)
infra
- contains actual implementation of the interfaces defined incore
. Package structure should be the same ascore
. Tests should for these classes should be component tests (using test containers/wiremock).api
- code for initiating the business logic / use case. Components are organized based on how it’s triggered (REST, messaging, etc). Should only depend on use case interfaces and util classesutil
- utility code for cross-cutting concerns
Sample package structure
ph.safibank.<name-of-service> api rest UseCaseController.kt messaging ReponseListener.kt ... core account AccountService.kt fraud ... usecase impl UseCaseImpl.kt UseCase.kt infra account AccountServiceImpl.kt fraud ... util Util.kt Application.kt
Sample code: https://github.com/SafiBank/SaFiMono/tree/main/services/inbound-transaction-manager
Test Driven Development (TDD)
Overview
Test driven development is a programming paradigm wherein you start by creating a test for the functionality before creating the actual implementation. Each test should be small and should validate a specific functionality.
The goal is for the design of our code to be consumer/user focused, since starting from the test places generally places you in the perspective of the user first. This should lead to us creating better designed, maintainable code.
General Steps
We want to follow a red-green-refactor approach when adding new code to our code base:
Create a test to verify a specific functionality
make the test small
only verify what is necessary for that test
make sure the tests can be run independently from one another
Run the test - make sure it fails for expected reasons (RED)
Add the minimum amount of code needed to make the test pass
Run the test again. All tests should pass. (GREEN)
If any of the tests fail, return to step 3.
Once all tests pass, refactor the code. (REFACTOR)
You can refactor either the implementation or the tests, though only do it one at a time.
Run the tests every time you make a change
General Development Cycle
Overview
We want to have the mindset when developing our code base:
consumer-driven
focused on what the users of our code actually needs
focused on what our core business logic actually needs
test-driven
create more confidence in our code
assume the perspective of the consumers by starting with the tests.
As such, we want to adopt a structure that enables both - hexagonal architecture and TDD.
Development Steps
Use TDD for every step
Start from the
core
Create a test for a specific use case
Define dependencies as interfaces and mock them with code level mocks in tests
Should have the most amount of tests
Create implementation for dependencies -
infra
Use component tests here (wiremock, test containers)
Create API to expose/trigger business logic use cases -
api
Should conform to API contracts defined with other consumers
Should have an end-to-end component test to check that wiring is correct
Further Reading
Hexagonal Architecture in Netflix - https://netflixtechblog.com/ready-for-changes-with-hexagonal-architecture-b315ec967749
Test Driven Design - https://en.wikipedia.org/wiki/Test-driven_development
Attachments:
code-structure-hexagon.png (image/png)
code-structure-hexagon (application/vnd.jgraph.mxfile)