Clean Microservice Architecture in Practice

Creating an application is easy, but creating a Great, Production-Ready Application is a whole different story.

Carlos Fau
etermax technology
10 min readFeb 21, 2020

--

Yes, a lot of (buzz?) words.

Creating an application is easy, just sit to code and … voilà. You have an application. If you are stuck, just go to the Internet, search, and you are going to find tons of samples, frameworks, templates, that will help you with your application.But creating a Great, Production-Ready Application is a whole different story.

Creating an application is easy, but creating a Great, Production-Ready Application is a whole different story.

To start with, we want to make sure that:

  • The app is correct (it just does what it is meant to do, and it does not do what it is meant to “not do”)
  • The app is performant (it uses only the allocated time and it consumes no more resources than the allocated to it)
  • The app will last (yes, we need the application to run not only the first, and (perhaps) the second day, but every day in his lifetime).

This last point is so critical that we additionally need to keep in mind that:

  • It should be easy to change
  • It should be easy to improve
  • It should be easy to expand

Creating an application is easy and (at first) very fast. Creating a Great, Production-Ready Application requires careful thinking, planning, and engineering. Yes, you can also go for the ‘Let it grow with time’ philosophy; nature has millions of examples in which this works out perfectly fine. The thing is, that it takes nature millions of years, and it is possible that you might not be willing to wait that long.

This publication shows an engineering based approach to help you in developing a Great Application, and some related stories on how we make the most of it.

Components

As it was described by Fred Brooks in his excellent article “No Silver Bullet”, the development of an application has problems related to the “essence” of the domain, and problems derived from our decisions (the “accident”).

The Essence will be described in the Model Entities and Use Case component. These are usually gathered as the “core”. Here is the domain knowledge and the “feature” we want to implement.

The Accident is composed by the rest: Delivery, Gateway, Repository, External Libraries, Configuration, and Main. All this is just because computer programming is not perfect, we need a lot of support to let the Essence work in a real environment. These components are known as the “Infrastructure”.

Model entities

Components

Entities encapsulate Enterprise wide business rules.

An entity can be an object with methods, or it can be a set of data structures and functions. It doesn’t matter as long as the entities can be used by many different applications in the enterprise. They represents the bare domain.

Use cases

Use cases contain application specific business rules.

It encapsulates and implements all of the use cases of the system. These use cases orchestrate the flow of data to and from the entities, and direct those entities to use their enterprise wide business rules to achieve the goals of the use case.

Delivery

In the Delivery we found the Interface Adapters that receives requests from the outside of the microservice and delivers (that’s where the name comes from) a response to them.

It is common to implement it as a REST HTTP Server, or consume Message from some Message Broker (like any JMS server, Kafka, etc.).

Gateways

Gateways are Interface Adapters that let this microservice request services to another microservices (or legacy or external systems).

It is common to implement them as a REST HTTP Clients, Message Broker Client, or any other API Client.

Repositories

These are Interface Adapters to systems meant to store and retrieve (serialized) application objects (usually entity).

The difference between repositories and Gateways, is that the Gateways usually communicate to other systems, but (in the microservice architecture) the Datastore (a Persistence Backend) is only meant to be used by this service, they belong to the Logical Boundary of this Bounded Context (see Domain Driven Design).

Configuration

The Configuration is the part of the system that composes the different components into a running system.

It contains the factories of all the components and performs dependency injection to link the different components in a way to assemble the application.

It also has the logic to gather configuration data from application parameters, environment variables, or external configuration files that are used in the process of assembly to let this ensemble be “parametrized”.

Main

The main is just the entry point to the application.

Its only purpose is to call the Configuration and kick the application running. At some point, the story should start.

Process flow

To provide the desired behaviour, these components interact between them. The flow of actions, in a typical reactive system, usually follows this pattern:

Request Process Flow
  1. An external system performs a request (An HTTP request, a JMS message is available, etc.)
  2. The Delivery creates various Model Entities from the request data
  3. The Delivery calls an Use Case Action (Command, Interactor, etc.)
  4. The Use Case operates on Model Entities
  5. The Use Case makes a request to read / write on the Repository
  6. The Repository consumes some Model Entity from the Use Case request
  7. The Repository interacts with the External Persistence (DB command, File System command)
  8. The Repository creates Model Entities from the persisted data
  9. The Use Case requests collaboration from a Gateway
  10. The Gateway consumes the Model Entities provided by the Use Case request
  11. The Gateway interacts with External Services (other applications, put messages in queues, print on a printer, etc.)

Dependencies

Following the ideas from previous architecture proposals, all dependencies go from the technicals details to the more conceptual entities. This rule is only broken for dependencies on supporting libraries, like the run time libraries and other useful and general ones (JSON conversions, Datetime management, Math, Collections, etc.).

Module dependencies

In the picture, the darker sections are related to more detailed and more technology specific processes (the Accident), while lighter ones are more conceptual (the Essence).

In detail:

  • Everything depends on the Business Model Entities, which should be the most stable elements, and the most important ones.
  • Delivery, Repository, and Gateways (the Infrastructure Layer in Clean Architecture and other models) depend on the Use Cases (Application Business Rules) and the Model Entities (1). These last two are the Core of the Business.

All the components can depend on supporting libraries:

  • Model entities require a collection framework to have lists, sets, etc. Also, they could need a Datetime support, some math, etc.
  • Use cases need support for collections processing, specific business logic, concurrency management (yes, it cannot always be set transparently), and others
  • Delivery needs HTTP Server support, JSON or other conversions, JWT validations, etc.
  • Gateways require HTTP Clients, Queue Server clients, etc.
  • Repositories require DB clients

(1) Many argue that model entities should not be visible from infrastructure. I’m not quit convinced that the gain in separation is worth the increased complexity when dealing with microservices. If the language supports it, some form of “immutable Interfaces” can help.

Kotlin sample implementation

To implement an application using this architecture style, we used the following details.

The application was developed in Kotlin (because not many people supported me on doing it in Scala) and used Vert.X framework for HTTP Client and Server, and ReactiveX (RxJava2) for concurrency. JSON was used as serialization mechanism.

In other cases we used the KTOR library for HTTP Client and Server and the native support for concurrency.

Model entities

The model entities are Plain Kotlin Data Classes (I don’t know if anyone coined the term PKDC, but POJO has more style and marketing look & feel). We made great effort to make this entities immutable (Why?):

data class QuestionWithImage(val id: String, val question: String, val image: URL, val answer: String) {
fun sanitizeAnswer(sanitizer: SpanishTextSanitizer):
QuestionWithImage = copy(answer = sanitizer.sanitize(answer))
}

Use cases

Use Cases are implemented through a collection of Actions (you can call them Interactor or however you want).

Sometimes if you need to extract common code from several actions, you can create a Service class to hold this common behaviour, but don’t always try to put an Action AND a Service because the layered Architecture ‘requires’ it.

A sample action:

class GetQuestionImage(
private val questionRepository: QuestionRepository,
private val geoIPService: GeoIPService,
private val usersRepository: UsersRepository
) {
private val sanitizer = SpanishTextSanitizer()
operator fun invoke(
ip: String, userId: Long, context: RequestContext
): Single<QuestionWithImage> =
geoIPService
.findCountry(ip)
.zipWith(usersRepository.findBy(userId, context),
BiFunction<String, User, Pair<String, Language>> {
country, user -> country to user.language
})
.flatMap { (country, language) ->
questionRepository
.findOneWithImage(userId, country, language)
}
.map{ it.sanitizeAnswer(sanitizer) }
}

In this case every Action was implemented as a ‘function’ using Kotlin’s “operator fun invoke”. Also in this application, the concurrency was managed by a Reactive library (ReactiveX) and the actions provided a result boxed in an Observable (a Single observable in this case).

The Action coordinates the behaviour of Model Entities, Gateways (like the GeoIPService) and the Repositories.

Delivery

The Delivery consists of a bunch of Handlers. Each Handler handles a specific requests, converts the request payload to DTO or model entities, and calls the appropriate Action. Finally, it converts the Action response into the response payload and sends it back it to the caller.

class UserConfigurationHandler(
private val getUserConfiguration: GetUserConfiguration,
private val jsonCodec: ObjectMapper
) : BaseHandler(jsonCodec) {
override fun register(router: Router) {
router.put(PATH).handler(protect{ context ->
handle(context)
})

router.get(PATH).handler(protect{ context ->
handleGet(context)
})
}
private fun handleGet(context: RoutingContext) {
val platform = context.getParamString("platform")
val country = context.getParamString("country")
try {
val vo = GetUserConfigurationVO(platform, country)
handleRequest(context, vo)
} catch (e: RuntimeException) {
onError(context, e)
}
}
private fun handle(context: RoutingContext) {
try {
val vo = jsonCodec
.readValue<GetUserConfigurationVO>(context.bodyAsString)
handleRequest(context, vo)
} catch (e: RuntimeException) {
onError(context, e)
}
}
private fun handleRequest(context: RoutingContext,
vo: GetUserConfigurationVO) {
val userId = context.pathParam("userID").toInt()
val ip = context.getIP()

getUserConfiguration(ip, userId, vo.platform, vo.country)
.subscribe(
{ onSuccess(context, it) },
{ onError(context, it) }
)
}
}

The Handler has no logic beyond the transformation (serialization, deserialization) and the call to the action. All the business logic should be behind the Action.

Gateways

class QuestionRepositoryRestClient(
private val restClientTemplate: RestClientTemplate,
config: RestClientConfig
) : QuestionRepository {
private val appId: String = config.appId
override fun findOneWithImage(userId: Long, country: String,
language: Language): Single<QuestionWithImage> =
restClientTemplate
.post("/games/$appId/users/$userId/questions/search",
QuestionsResponse::class,
QuestionsRequest.forImage(country, language.toString())
)
.map(this::toQuestionWithImage)
override fun confirmQuestionsAnswered(userId: Long,
questionIds: Iterable<String>): Single<Unit> =
restClientTemplate
.post("/games/$appId/users/$userId/answers",
AnswersRequestResponse::class,
AnswersRequestResponse(questionIds.toList())
)
.map{ Unit }
}

Here the Gateway takes advantage of a provided RestClient to connect to an external service.

Once more, the Gateway has no Business logic, but conversions and mappings.

Repositories

In this case the repository was small enough to be held in memory and it does not need to be persisted. In case the application goes down, the data is lost and regenerated again.

class InMemorySuggestedOpponentsRepository
: SuggestedOpponentRepository {
class LanguageEntry(val set: MutableSet<Long>,
val queue: ConcurrentLinkedQueue<Long>)
private val emptyEntry = LanguageEntry(
mutableSetOf(), ConcurrentLinkedQueue()
)
private var suggestedUsersByLanguage
: ConcurrentHashMap<Language, LanguageEntry> =
ConcurrentHashMap()
override fun save(userId: Long, language: Language): Single<Long>{
suggestedUsersByLanguage.compute(language) { _, ids ->
val entry = ids ?: LanguageEntry(
mutableSetOf(), ConcurrentLinkedQueue()
)
if (entry.set.add(userId)) entry.queue.add(userId)
entry
}
return Single.just(userId)
}
override fun find(userId: Long, language: Language)
: Single<Optional<Long>> {
val entry = suggestedUsersByLanguage
.getOrDefault(language, emptyEntry)
val opponentId = entry.queue.poll() opponentId.let { entry.set.remove(opponentId) } if (userId != opponentId) {
return Single.just(Optional.ofNullable(opponentId))
}
val otherId = find(userId, language) save(userId, language) return otherId
}
}

The repository implementation is not thread safe, because it is meant to live inside a Vert.X verticle that provides the isolation and serialization needed.

Configuration

Many people ask ‘Why didn’t you use Spring (Framework)?’. There was a year in which “Dependency Injection” was synonym of ‘Spring Framework’. Spring is a terrific framework that helped improve and produce great quality applications solving tons of problems.

But what happens if you don’t have those problems any more? Do you still need a (complex) framework at all?

This is a sample of how to have Dependency Injection without a library and without Spring (ohh my God, is that possible?)

object Repositories {
private val webClient by lazy {
WebClient
.create(vertx, WebClientOptions().setFollowRedirects(true))
}
val jsonMapper: ObjectMapper by lazy {
ObjectMapper()
.registerModule(JavaTimeModule())
.registerModule(KotlinModule())
}
val questionRepository by lazy {
QuestionRepositoryRestClient(
restClientTemplate, config.platform
)
}
val suggestedOpponentRepository
: SuggestedOpponentRepository by lazy {
VerticleBasedSuggestedOpponentRepository(vertx.eventBus())
}
val suggestedOpponentsVerticle by lazy {
InMemorySuggestedOpponentsVerticle(suggestedOpponentCache)
}
val inMemorySuggestedOpponentsRepository by lazy {
InMemorySuggestedOpponentsRepository()
}
private val suggestedOpponentCache by lazy {
SuggestedOpponentCache(
FillCacheIfNotFullEnough(
inMemorySuggestedOpponentsRepository,
config.suggestedOpponentConfiguration
),
config
.suggestedOpponentConfiguration.minutesToNextSuggestion
)
}
private val restClientTemplate by lazy {
RestClientTemplate( webClient, jsonMapper, config.platform)
}
}

Ohh, wow, a lot of Dependency Injections without frameworks !!!.

Yes, this schema has some drawbacks, for example it does not handle cycles gracefully. But having no cycles is cool, isn’t it?

We use the Kotlin support for lazy initialization that allows us to not sort the factories (yes, those lines are inline factories) in topological order.

Main

Finally, the main class ends being a very boring one:

object Application {
@JvmStatic
fun main(args: Array<String>) {
vertx.deployVerticle(
ServerVerticle(DeliveryProvider.routes, Environment.PORT)
)
vertx.deployVerticle(suggestedOpponentsVerticle)
}
}

Conclusions

Creating Great Applications is challenging, but a good structure and the correct following of some principles can help a lot in its development. The shown architecture let us create applications that are easy to understand, easy to change, and easy to fix. The correct separation of concerns provides us a solid foundation, and also allows us to do it without complicated frameworks or libraries.

Thanks for reading and hopefully this material can help you.

In a following article you will be able to see how to ensure that this architecture is followed: Checking the architecture with tests.

Authors: Carlos Fau

Thanks to:

  • Mercedes Carrocio for English style and typo corrections
  • Matías Leiva for the corrections.
  • Matías “Professor” Zuccotti for the art magic.
  • Martin Gonzalez for valuable comments.

--

--