Practical experience of building microservices in Go

mikebolshakov
5 min readApr 15, 2021

--

“Success is the ability to go from one failure to another with no loss of enthusiasm”

Sir Winston Churchill

Each time starting a new project you are trying your best to avoid all the issues you faced your prior projects, but inevitably generate new issues…

In this post I want to share some of the architectural decisions made by me in the recent project. Mainly I will touch an internal structure of microservices written in Golang. Please note it doesn’t follow any particular architectural patterns but rather inspired by DDD/Onion and my own experience.

Principles

So, lets start from points I intended to address with this design:

  • Services must have similar structure to be easily understood by newcomers and templated. Besides, I hope to speed up the development process by standardization
  • Logical layers must be separated as much as possible to be testable and follow “separate and conquer” strategy
  • I want to follow “domain-first” principle as my domain logic is the most important and tricky part. So, all decisions are made to make writing and testing domain logic as easy as possible
  • Services communicate in sync and async manners (gRPC and messaging correspondingly). It’s ok to have many-to-many relations between services, but the architecture must make a domain logic not to be dependent on any external artifacts (other services, storages, etc.). The idea is to keep domain logic is fully testable and all external dependencies are fully mockable
  • Initially, I have no idea what persistence technologies would be employed, so infrastructure code must be easily exchanged
  • Service must provide as many public API as needed for external consumers. API layer interacts with a domain layer only, no other dependencies with implementations
  • I don’t want to use any DI containers to keep things as simple as possible, but I also don’t want my service’s internal dependencies to turn into a mess. So, the rules of bootstrapping must be clearly specified
  • Contracts must be clearly specified and ensure the only point of truth
  • Infrastructure code must be reused whenever possible

Note, not all these principles are typical for golang applications but I don’t care I follow my experience and intuition… Go ahead!!!

Implementation

Having points declared above in mind lets have a look at the design I finally came up

  • Infrastructure code is located in a separate repository (kit). So, no teams must invent their way of communication with PostrgeSQL or Kafka or smth else. They just need to import a module and make use of it. The drawback of this is redundant dependencies
  • All contracts (protobuf and message schemas) are also put in a separate repository (proto). One more benefit of having contracts in a separate repository is that you as an architect could easily keep all the changes made in contracts under your strict control
  • Services are built based on the same template which includes several logical layers and rules specifying how these layers communicate with each other. The layers are:

API — provides a public interface based on the declared contract. It imports domain only. It has no knowledge about other layers. It plays “gateway” role: get request -> convert ->validate -> pass to domain -> get response -> handle errors -> pass to client

Domain — specifies domain and repository interfaces. (You may ask why repository interfaces are specified here… Because it’s up-to domain layer to decide what it needs for itself. So, any repository implementation must satisfy a contract the domain specified). Also, a domain layer implements business logic by importing repository interfaces

Repository — responsible for external communications with either other services or infrastructure components. It implements repository interfaces specified in domain layer, so we get a picture where domain is completely independent from repositories

  • Bootstrapping. Simply put bootstrapping is implemented by a single go file (service.go) which is located in the root folder of a service and imports all the artifacts. It has several predefined methods that determine a pipeline of a service:

New — responsible for instantiation of everything

Init — responsible for initialization of everything including dependencies between layers

Listen — starts all the background processes (listens for HTTP or gRPC connections etc)

Close — responsible for graceful closing

Pipeline is invoked from main.go so that main func has repeatable straightforward logic

  • Each dependency for another service requires an adapter which implements details of communication and converts input/output between domain expectations and an external service contract. It allows to mock adapter interface or change it with another implementation without domain affection
  • Apart from service dependencies there are also dependencies on infrastructure components. But the idea is the same. We hide communication details behind an adapter which may have as much complex logic is required like retry, readiness / liveness probe, connections reestablishment etc.

Please, look at the figure below depicting the details described above

Drawbacks

While I’m ok with this design and it got through some of the real life scenarios there are several drawback revealed

  • A lot of boilerplate duplicated code in adapters. So, for each new dependency we must introduce a new adapter. While all of them are more or less the same and they are well templated, it may bring some inconveniences especially for refactoring
  • Bootstrapping logic tends to be complicated. Ok, bootstrapping logic is always complicated and that is why DI frameworks were invented, but here we keep the idea not to use DI and benefit from explicit dependencies
  • Sometimes one of your repository adapter wants to communicate with another and that is what not intended by design. So, we solved it by either async communications via messaging or given a chance to domain to manage such relations, but in general it’s a smell and needs to be avoided
  • There are some cases where an adapter wants to notify a domain about something. So, the easiest way to handle this to employ “publish-subscriber” pattern where domain specifies events and repository subscribes on these events. But anyway it complicates relations

Conclusion

In this post I touched only high level of design.

I would be glad to see any questions or feedback, especially highlighted possible issues you see in this design.

Thanks for your attention

--

--

mikebolshakov
mikebolshakov

Written by mikebolshakov

Golang developer & system architect

Responses (2)