Following are the ideas I have gathered while working with microservices at Wise (former Transferwise).
TODO
There are 2 factors to consider when deciding how big/small should a service be: technical scalability and organisational scalability. If neither of these is pushing for a split it is better to keep going in a monolith. There is no reason to assume that if we are unable to design things well in a single codebase we will be able to design things well in a distributed codebase. Read more
If you indeed need to split things into microservices be careful not to replicate the same antipatterns you might have in the current (legacy) codebase. Otherwise, you may just replace the monolith with a distributed monolith.
Having as few synchronous calls between services as possible is always something to strive for. Using Kafka to build local replicas of data is one solution. Often Kafka is an obvious solution where we need complex data transformation or aggregation of multiple sources. However, it is also very handy for avoiding single source sync data fetching as well.
Avoid technical suffixes like Service, Gateway, Repository and instead search for the domain specific names. Names are very powerful - just like they can work as central cores for bringing more order into things they can also make things messier and harder to understand.
Try to design your domain model so that it is impossible to use it incorrectly.
One such example is using separate classes for different entity states
There are many situations when using Hibernate is not a good idea. For very simple domain models that clearly map to DB tables ORM may be OK. However, when domain model grows in complexity there will be more and more constraints from using ORM.
There are some more lightweight tools for accessing relational DBs like jOOQ or myBatis. Don't have any first-hand experience in using these, but I recommend thinking carefully if the benefits of these libraries are worth the potential constraints they may be imposing for more complex use cases.
An idea how to manage complex data mapping to domain model using specialised factories
Often there is separate Service class for each Entity which then encapsulates all access to that Entity. If there is no additional logic in that Service then it should not exist. Relaxed layering FTW!
The benefits are written here.
Whenever possible prefer using config as is inside code as opposed to external. Read this post for more details
Always avoid any nulls. Instead use NullObject and Optional. Avoid NotNull annotations and instead use explicit Option types.
One option how to package is following the Hexagonal architecture. Top level will have all the feature packages if the service has more than one module. I think it is perfectly valid that a microservice contains multiple modules. No need to split the system into powdery pieces as noted in [Microservice size]
Then under each module package we can have all stuff needed for that specific module. Note that this approach also prepares us for possible future split if the team or team cognitive load grows too big.
Good resource for pragmatic Hexagonal architecture: Getting your hands dirty on Clean Architecture.
Try to come up with clear categories how your team classifies tests. Otherwise, you may end up with all sorts of weird creations that do some "unit test" things and some "integration test" things all at once. Not having clarity means more work when deciding what tests are needed for any new feature but also harder time understanding existing tests.
TDD is good, but it is also very hard. Even when failing to do TDD by the book I have found it valuable to listen to what the tests are trying to tell me. If you do that then unit tests can be great source for pushing our design.
If better design by tests is not something you want then it might be better to focus on writing more coarse-grained tests. In case of Hexagonal architecture port level tests are good for driving high level architecture.
In any case I recommend not writing tests against configuration.
In Growing OO Software Guided by Tests Steve and Nat suggest the idea of starting with a high level acceptance test before getting into new feature implementation. One easy way of knowing when you have enough acceptance tests is when you don't feel like you should do some manual testing before releasing.
One question with acceptance tests is the level at which these should be written. Steve and Nat suggest writing them as e2e as possible. However, sometimes it can be more effective to write these tests at your port level (or whatever is the facade to the domain model). It is just much more convenient to write tests through Java API than pushing everything through some kind of HTTP API. To push it even further we can write acceptance tests without any framework dependency. We don't really need Spring for verifying our business logic (at least hopefully we don't). For wiring things together we can just instantiate our Java config objects and get the port instance without Spring.