Business objectives and technical implications
We can see that a lot of new fancy frameworks and solutions appears each year, but instead looking for something new and popular that should magically solve all our problems let”s stop for a bit and try to define our problems and what we actually need. The goal is of course to maximise the company profit. So, what will help to achieve it from the software side:
Ideally developers should focus only on business logic (boring but it is only what brings money)
As little as possible technical challenges (junior developer should be able to do most of the work as good as a senior)
Easy to understand, maintain and extend (longer to onboard people or higher qualifications required — less profit)
Easy to test to ensure quality
Possibility for horizontal scalability for growing number of clients
For the beginning, it”s important to understand that there is no silver bullet for 100% of the cases. So, let”s define our scope first, or better what will be out of scope. We will consider remote multi‑client system with no low latency requirement (up to 500ms worst case per user request should be acceptable).
System architecture overview
Let”s think about the architecture. Monoliths are quite complicated (at least when the application starts growing) and poorly scalable. Thus, we need distributed system, but to make it simple and easy to maintain will take anemic immutable model for implementation (will talk in detail a bit later). And if we can make services stateless, it will provide easy horizontal scalability.
Our system can interact with 3rd party systems and that interaction can constantly change (for example, if we”re developing store management system and it must interact with supplier systems. Suppliers can be changed several times per year). Thus, we need clear separation between our domain and integration layer. No 3rd party models should be used in our domain services. Otherwise, each change of 3rd party will result in the refactoring of the whole system that we definitely want to avoid.
Other point to consider, is that we want single auth for all the system services for better user experience and to avoid coping same auth logic to each of the services. For that we can introduce api‑gateway service as single‑entry point for the external calls.

On the diagram we have connectors — the goal of this services is to collect data from the 3rd party and transform to our unified domain model (and in opposite direction). One connector per one 3rd party system. It also should allow our system to continue working if some of the connections are down (some flows of course will not be acceptable, but it won”t affect others). The connectors don”t have any DB and don”t persist anything, they are responsible only for transferring and transforming the data.
Other components on the diagram:
API Gateway — Central entry point handling auth, routing, and rate limiting
Core Services — Stateless business logic components
Mapping Service — Centralized transformation rules repository
Storage — Optional database modules for cross‑service data
Component Design Principles
Now, let’s look into design of the components themselves. First, the idea is to keep them stateless with the exception of in‑memory cache or other infrastructure optimisations (rate limit, websocket connections, etc). Thus, with no state and be just part of the data flow (this called transaction script in DDD) it”s easy to operate with immutable models, and as they can”t change their state it makes sense not to keep any logic there as well. So, all models are POJOs. There are other benefits of immutable anemic models such as thread‑safe by design that helps easy scaling and predictable inputs/outputs allow to simplify testing (TDD works much easy with it).
We”ll split each component for modules:
API — module that contains only models that used for input/output of our service, shouldn”t depend on anything other that apis modules of other services
APP — startup of the services and configurations. Could also have presentation layer (rest controllers for example) but no business logic.
CORE — contains the business logic
STORAGE — Repositories that write to DB or other storage. The reason to keep this module separate is to reuse it for several services if they need to operate with the same storage, like when you want to split input/output flows to horizontally scale one part. Doesn”t needed if service doesn”t have any storage at all.
CLIENT — could be more than one module. It is providing wrapper on transport layer to easy use from other services. Can’t depend on service modules except API.
CORE and APP modules never can be used as dependencies for other services. It’s important to notice that neither whole CORE, nor its parts can be reused between services, as it’s a business logic. Especially that is paramount for connectors as different 3rd party can behave similar. However, as it can be changed independently it can cause complications in the future if will be extracted. Never try to extract business logic to libraries - either merge the services to one if the behaviour is guaranteed to be the same or just copy/paste otherwise.
Module | Can depend on | Can’t depend on |
API | Other API modules | Everything else |
CLIENT | API modules | CORE, APP |
CORE | API, STORAGE | APP, Other CORE |
Technology selection criteria
The system design that way can easily contain components in different languages. However, more different languages, frameworks, approaches are used – more difficult to support. So, better to limiting variety in the system.
Thus, we need to limit what we will use for majority of our system. Scripting languages - basically not suitable for anything other than small scripts. Dynamic types can cause lots of bugs in runtime, code become extremely difficult to maintain as changes can cause completely unexpected side effects. So, that type of languages we better avoid using in production at all. (Python, Groovy, etc)
System language is great choice for writing software for microcontrollers, implementing OS or extreme low-latency, but they add a lot complexity, and for most goals their benefits are actually useless. Thus, it better to use them only when you do have specific requirements that they can help with, and definitely not the main choose (Zig, Rust, C/C++, etc).
This leave as with general propose group like Java, Kotlin, C#, etc. It better to consider language with the richest ecosystem to avoid spending time on infrastructure tasks. Also, the ecosystem should be suitable with microservices (that is the problem with C# as it designs primarily for monolith and rich data models). Of course, due to all of that, jvm based languages will be a great choice. Probably Kotlin is slightly better due to less boilerplate and more advance features. Null safety makes maintenance easy as well.
Moreover, don’t disregard huge frameworks like Spring or Micronaut as they can drastically speed up all infrastructure setup for your services. Main issue why people usually don’t like them is due to number of dependencies that they bring that cause RAM overhead and some performance overhead. However, it’s important to understand is that overhead makes any difference for your application or not, as in majority of applications it won’t be noticeable at all. On the other hand, they provide a lot of integrations that you need out of the box, including monitoring that is necessary for any service in prod. They can save multiple weeks on developing all infrastructure.
Benefits
As example for what we are achieving by implement system that way:
Small microservices and well-known technologies reduce onboarding from couple of weeks to couple of days (that is several thousand pounds per developer)
Simplicity of services design allow juniors to work on business logic as well efficient as seniors
Horizontal scalability around 40% cheaper than vertical scaling
3rd party isolation prevents 200+ hours/year refactoring per integration