An In-Depth Look at Microservices Design in Zeals using Clean Architecture

Yoshimasa Hamada
The Zeals Tech Blog
10 min readOct 15, 2021

*This article was originally posted in December 2020 and has been partially revised and translated into English.

This year, due to coronavirus, there have been a lot of changes in various industries, but Zeals has been able to grow a lot in the past year.

In this entry, I’ll be talking about the development of Microservices (in particular its design methods and practices) that we started back in late 2019.

TL;DR;

  1. Adopt clean architecture and let’s make microservices resistant to change.
  2. The most important thing when designing microservices are entity!
  3. Usecase is a service instruction. The importance of documentation.

Background

First, let’s briefly review the situation in mid-2019 and then move on to understanding the background of Microservices.

Originally, the chat commerce service, Zeals, was made up of two core applications. One application was an internal management screen for a chatbot developed using Ruby on Rails and React. The main users were the members in charge of scenario design. They were used to set up the best user stories for each client and to set up bulk distribution. The other application was a server developed in Python that sends and receives messages to the chatbot. The application interacted with the APIs of platforms supported by Zeals, such as LINE and Facebook.

At first glance, it looks simple, but in order to respond to internal and client requests at a fast pace, a variety of features were added, such as flexible delivery methods and rich message expressions. Naturally, these domain logics were scattered across multiple applications, and some of the functions had the exact same logic written in Python and Ruby.

It was obvious that further development in this state would make it difficult to maintain the two applications and they would both be susceptible to frequent changes. Also, at the time, the performance of the delivery process was not a major concern, but with the expectation of large scale clients, we needed to improve the scalability of certain functions.

With this in mind, we began to shift the project toward becoming Microservices.

How Should We Design Microservices?

The first thing to consider when moving forward with Microservices are the design principles. If you design Microservices without a clear intention or axis, you will end up with Microservices that are just common domain logic.

Since Microservices will be called on by various applications, the following requirements were set as the core of the design:

  • Intuitive interface
  • Freedom to change the internal implementation
  • No impact on the caller as long as the Interface does not change

All of these are general Microservices design principles, but you should always be aware of them when designing. In particular, the second and third principles are very important. If the second and third principles are properly designed, it is possible to “rewrite partial components at the language level to improve performance,” as in the Discord example below.

At the time, when I was thinking about the second and third principles, Clean Architecture naturally came to mind (because at the time I was also reading “Clean Architecture: A Craftsman’s Guide to Software Structure and Design”).

I won’t go into detail about Clean Architecture itself in this entry, but I believe the underlying design philosophies are:

  • Make the software resistant to change.
  • For this purpose, the dependencies between components should be changed from those that are easy to change to those that are hard to change.

The following is a famous diagram that Robert C. Martin, a proponent of Clean Architecture, posted on his blog, but in this entry, I would like to focus on Entities and Use Cases.

https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
  • Entities are models of the people, things, concepts, and business rules that the service deals with.
  • Use Cases use Entities to package the sequence of processes provided by a service

Please recall the design principles of Microservices here.

Isn’t it easier to understand if you think of “Intuitive Interface” as “pure business rules that don’t rely on Database or Programming Language”?

In addition, in Clean Architecture, Entity and Usecases are defined as implementation independent. Therefore, if we match the Microservices Interface (specifically, the gRPC Service) with the Usecases, we can make it so that "as long as the interface does not change, the caller is unaffected.

At the time, I felt a sense of comfort, like when the pieces of a puzzle fit together perfectly. I was sure that designing Microservices based on the Clean Architecture would make them easier to use and more resistant to change.

Practices for Microservices Design

Here are some practices for designing Microservices based on Clean Architecture.

In the design of Microservices, the Entity and Usecase of Clean Architecture are related. Entity design, in particular, is a very important and difficult topic. The ease of use of Microservices as a whole depends on how well it is designed.

What are Protocol Buffers?

Since Zeals uses gRPC, it uses Protocol Buffers to actually define Entities (If you are already using Protocol Buffers, you can skip this section).

As a brief introduction, Protocol Buffers are an IDL (Interface Description Language) that are independent of programming languages and platforms. It’s well known as an IDL for gRPC (also developed by Google), but it is also excellent as a standalone IDL. Personally, I feel that once you get used to Protocol Buffers, it is hard to go back to OpenAPI Specification (Swagger).

Entity Design

First, let’s take a look at a basic gRPC schema definition that is not based on the Clean Architecture. As a sample, I will show you the Basics tutorial provided by the official gRPC documentation.

The following are some of the .proto files that are being used. (To see them all, click here)

What is defined as a message is a so-called model. When you actually generate the code for each language, a class is generated for JavaScript, and a struct is generated for Golang.

// Points are represented as latitude-longitude pairs in the E7 representation
// (degrees multiplied by 10**7 and rounded to the nearest integer).
// Latitudes should be in the range +/- 90 degrees and longitude should be in
// the range +/- 180 degrees (inclusive).
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
// A RouteSummary is received in response to a RecordRoute rpc.
//
// It contains the number of individual points received, the number of
// detected features, and the total distance covered as the cumulative sum of
// the distance between each point.
message RouteSummary {
// The number of points received.
int32 point_count = 1;
// The number of known features passed while traversing the route.
int32 feature_count = 2;
// The distance covered in metres.
int32 distance = 3;
// The duration of the traversal in seconds.
int32 elapsed_time = 4;
}

Next, we define service as the RPC (Remote Procedure Call) Interface expressed by using message.

// Interface exported by the server.
service RouteGuide {
// A simple RPC.
//
// Obtains the feature at a given position.
//
// A feature with an empty name is returned if there’s no feature at the given
// position.
rpc GetFeature(Point) returns (Feature) {}
// A client-to-server streaming RPC.
//
// Accepts a stream of Points on a route being traversed, returning a
// RouteSummary when traversal is completed.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
}

Looking at this alone, you may be thinking, “Do we really need such an exaggerated design concept as Clean Architecture?”

However, the definition of a message is very different depending on whether you are aware of the Entity of Clean Architecture or not.

And what do I mean by this?

In many cases, when you implement Microservices, your service has already grown to a certain scale. In other words, the schema definition already exists as a class of the programming language you are employing or as a table in the RDB.

So, when defining messages in Protocol Buffers, if you don’t have anything in mind, don’t you want to just define the existing schema definition as a message?

Please remember the principle that the “Clean Architecture Entity should not be dependent on a particular technology or platform". If you reuse existing schemas, you will end up defining more than a few Entities that violate this principle.

In fact, Zeals uses RDB (MySQL) as its main database, and table definitions are managed by Ruby on Rails Active Record. If we were to use this Ruby class for messages, we would end up with “an Entity that depends on the RDB.”

A more concrete example is an intermediate table in an RDB. For example, suppose we have an intermediate table called user_chatbot_associations to represent a many-to-many relationship between the users table and the chatbots table. This is common in RDBs, but it should not be represented as Entity alone.

This is because, if it were a document DB, you could design it as “just add a field like chatbot_ids to the users document" without creating such an intermediate table. In short, you're taking what Clean Architecture calls the Infrastructure layer (programming languages, libraries, databases, communication protocols, etc.) and bringing it to the Entity layer.

However, I think that this kind of database dependency is an easy topic to solve in Entity design.

In Zeals, chatbot-specific Entities such as the following need to be designed very carefully:

  • Multiple chatbot platforms
  • UI components in the chat (message text, images, buttons to select user responses, and even the layout)

At present, Zeals provides services only on LINE and Facebook, but there is a possibility that we will provide a third or fourth platform when we consider overseas expansion. We must also envision providing our own brand of chatbots and developing into the true sense of a platformer.

Naturally, API specifications differ for each platform, and the attribute information of chatbots and user information also differs. Entity design is required to abstract them appropriately and ensure scalability. (Otherwise, you can imagine the chaos in the code with more if statements as the number of platforms increases haha.)

In terms of UI components, there is a big difference in expressiveness between platforms: Facebook provides only simple and generic layouts, while LINE and other platforms only allow rich content expression. Flex Message, in particular, allows you to write CSS and configure the layout freely, and I still don’t know how to express it as Entity haha.

In the first place, the Entity of Clean Architecture claims to be “independent of any particular technology or UI”, yet it must represent a UI component…

What a contradiction!

This is the reason why I wrote at the beginning of this article that “Entity design is very important and difficult.” This is probably one of the most difficult aspects of Microservices development, but also one of the most interesting.

Usecase Design

So far, we have introduced Entity design with concrete examples. The next step is to define the service of gRPC, which is the interface of Microservices, with Protocol Buffers. Based on Clean Architecture, this service definition corresponds directly to Usecase .

However, if the Entity design is done properly, the Usecase will naturally be independent of any particular technology or implementation.

Now, the important thing here is the documentation, specifically the comments in the .proto file. Since the parameters and return values of RPC should basically be Entity and its fields, the comments should also be implementation-independent. More importantly, it should express what the RPC should be called for and when, even if the implementation is not known at all.

As an example, here are some comments on “RPC to narrow down the target users for bulk delivery based on predefined conditions.”

service EndUserService {
// Filter filters a list of end users related with a given chatbot ID by using a given filter ID.
// End users is filtered by the following conditions.
// 1. Whether the attribute ID of the end user matches filter’s one.
// 2. Whether inflow date and time of the end user matches filter’s one.
// 3. Whether the chatbot has permission to send a message to the end user.
// 4. Whether the end users belong to the specified percentiles of CV prediction score.
rpc Filter(FilterRequest) returns (stream e.EndUser) {}

All comments are written in such a way that they can be understood by those who know the Zeals service domain and business logic (expressed in Entity), without mentioning the specific implementation. This means that it can be understood by non-engineering members of the sales and scenario design teams.

If this can be achieved, we believe that it will be a very user-friendly Microservices Interface. We also hope that one day the .proto file itself will become a "Zeals service manual". When the system becomes larger than a certain size, the problem of "documentation deviating from implementation" will appear, but if it is written near the code, there is no need to worry about that.

Conclusion

In this entry, we introduced the design and practices of Microservices based on Clean Architecture.

In fact, the development of Microservices in Zeals is still in the beginning stages, so there will be more trial and error in the future. (In particular, the granularity of the usecase, i.e., the responsibilities that each RPC has, etc.)

Nevertheless, I believe that the system will grow to support the growth of Zeals as long as we do not stray away from the axis of “making it resistant to change”, which is the basis of Clean Architecture.

--

--