The Journey Towards Microservices

0
95

Designing applications around microservices architecture decomposes a monolith into a set of smaller and collaborative services. The icing on the cake is that we can evolve and scale each of the microservices independently of each other. This transition leverages the strong foundation laid by the object-oriented and domain-driven design patterns. This fifth part of ‘The Design Odyssey’ series demonstrates the evolution of a user management system (UMS) into a bunch of microservices.

For many, microservices are associated with Web applications, cloud services or at least REST API. Though there is a basis for such an impression, microservices need not always be designed in that way.

What is a microservice?
The basic premise of a microservice is that (in the words of Jeff Bezos of Amazon): “A team that can be fed with just two pizzas should be able to handle a microservice.”

A monolith is a relatively large and complex application that is developed and deployed as just one unit, as shown in Figure 1. It offers multiple functionalities and that’s why it tends to be huge, heavy and rigid. For example, it may be in the form of a single large WAR deployed on a Tomcat Web server. When the load on the server increases, the performance of the monolith falls. Also, adding new features to a monolith is a daunting and time-taking task.

UMS as a monolith
Figure 1: UMS as a monolith

When a monolith is decomposed into a set of smaller microservices, they evolve independently of each other. The microservices are usually aligned with the lines of business of the organisation. Each microservice covers only a specific business requirement. It corresponds to the concept of a sub-domain in the domain-driven design. It will have a domain model specific to its sub-domain. As we all know, a smaller domain model is easy to understand, develop and deploy.

Each team can choose the best platform suitable to develop its specific microservice; yet the services at runtime collaborate with each other to deliver the business value. New services can be added easily and deprecated services can be decommissioned effortlessly.

These services may be exposed via a REST interface, messaging interface or any other interface to the Web clients, mobile clients, IoT clients or standalone clients.

When the microservices are containerised, orchestrated and deployed on cloud infrastructure, they can easily be replicated to scale horizontally!

Service decomposition
The first step in the journey towards microservices is to decompose the monolith. A careful inspection of the monolith gives us an idea of how to peel the slices of functionality from it and convert them into independent microservices.

The user management service (UMS) that we developed as a monolith in the previous parts of this series offers three primary functionalities: 1) To add users to the system; 2) To find a specific user in the system; and 3) To search for users based on some criteria. Apart from these, the UMS is also logging the user-addition events in a central place. Let’s name them AddService, FindService, SearchService and JournalService. Refer to Figure 2.

UMS as a set of microservices
Figure 2: UMS as a set of microservices

Once the UMS is decomposed into the above four smaller microservices, each of them can be developed on different platforms in case such a polyglot environment is beneficial. For instance, AddService may be developed using Spring Boot on Java platform, FindService may be developed using Express on Node platform, and SearchService may be developed using Flask on Python platform.

The point worth noting is that these services are developed independently, even if all of them are developed on the same platform. The AddService might be released first, followed by the FindService. They do not share any codebase. Code reusability takes the back seat and service independence occupies the driving seat!

AddService

It’s time to design the microservice for adding users into the system. The design still sticks to the fundamental principles of object-oriented design. In other words, we will follow the SOLID principles and use patterns like factories, adapters, etc.

By taking inspiration from the Onion model, which we have seen in the past as part of domain-driven design, AddService is also modelled on the lines of domain layer, application layer and infrastructure layer. The domain layer consists of only domain objects. The application layer acts as the gateway to the domain layer. And the infrastructure layer offers all the generic services.

The domain model
This layer primarily consists of entities, value objects and repositories. The domain model is presented in Figure 3.

 The domain model of AddService
Figure 3: The domain model of AddService

As explored in the previous part, an entity is a domain object that is uniquely identifiable and mutable. What else other than the user can be an entity in the domain of AddService? Though the name of a user is not unique in the real world, for the sake of simplicity, let us treat it as the identity of the user. The identity must be immutable, but the rest of the entity can be mutable. For instance, the phone number of the user can be changed at any time.

Unlike the entities, value objects do not have any unique identity and are comparable only by their values. Usually, the primitive types with some sort of constraints are modelled as value objects. For example, the name of the user is modelled as a value object Name, since it is a string with some constraints like minimum length, maximum length, accepted characters, etc. Similarly, the phone number also can be modelled as a value object PhoneNumber, since it is a long number with constraints like length, positivity, etc.

Since the value objects are immutable, they do not offer any way to change their internals. In other words, they only offer getters, but not any setters.

A repository acts as if it is a cache of aggregates of domain objects. The UserRepository is one such repository that saves the user objects. Though the repository belongs to the domain model, the implementation is technology-specific. It may be implemented around MongoDB, MySQL, Cassandra or any other data storage technology. Since the domain does not depend on the technology, the repository implementation UserRepositoryImpl is kept out of the domain model.

The following is the implementation of the domain model on Java platform. The Name, being a value object, offers only a constructor and getters. The constructor may validate the input parameter for constraints before accepting. In case of an invalid parameter, an exception may be thrown. Also, since a value object is only compared with other objects by their values, the hashCode() and equals() methods are required to be implemented.

public class Name {
private String value;

public Name(String value) {
//TODO: validation
this.value = value;
}

public String getValue() {
return this.value;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((value == null) ? 0 : value.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Name other = (Name) obj;
if (value == null) {
if (other.value != null)
return false;
} else if (!value.equals(other.value))
return false;
return true;
}
}

Like in the case of Name, the PhoneNumber is also equipped with constructor, getters, hashCode() and equals() methods, as it is also a value object. Here as well, the constructor may validate the input value.

public class PhoneNumber {
private long value;

public PhoneNumber(long value) {
//TODO: validation
this.value = value;
}

public long getValue() {
return this.value;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (int) (value ^ (value >>> 32));
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
PhoneNumber other = (PhoneNumber) obj;
if (value != other.value)
return false;
return true;
}
}

The User is an entity! Hence, apart from getters, the setters are also considered. However, as the identity is supposed to be immutable, there is no setter for the Name property.

package com.glarimy.ums.domain;

public class User {
private Name name;
private PhoneNumber phone;
private Date since;

public User(Name name, PhoneNumber phone) {
this.name = name;
this.phone = phone;
this.since = new Date();
}

public User(Name name, PhoneNumber phone, Date since) {
this.name = name;
this.phone = phone;
this.since = since;
}

public Name getName() {
return name;
}

public PhoneNumber getPhone() {
return phone;
}

public void setPhone(PhoneNumber phone) {
this.phone = phone;
}

public Date getSince() {
return since;
}
}

Following is the simple InvalidUserException class that merely extends the system defined Exception class:

package com.glarimy.ums.domain;

@SuppressWarnings(“serial”)
public class InvalidUserException extends Exception{

public InvalidUserException() {
super();
// TODO Auto-generated constructor stub
}

public InvalidUserException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
// TODO Auto-generated constructor stub
}

public InvalidUserException(String message, Throwable cause) {
super(message, cause);
// TODO Auto-generated constructor stub
}

public InvalidUserException(String message) {
super(message);
// TODO Auto-generated constructor stub
}

public InvalidUserException(Throwable cause) {
super(cause);
/ / TODO Auto-generated constructor stub
}
}

The UserRepository interface just offers the contract for the repository. Since the AddService only saves the user data, the repository is sufficient to offer only that one operation.

package com.glarimy.ums.domain;

public interface UserRepository {
public User save(User user);
}

The UserRepositoryImpl somehow saves the user data using technology-specific implementation. For example, it may use JDBC to save data in an RDBMS. We will discuss the data layer at length in future.

package com.glarimy.ums.data;

import com.glarimy.ums.domain.User;
import com.glarimy.ums.domain.UserRepository;

public class UserRepositoryImpl implements UserRepository {

@Override
public User save(User user) {
// TODO
return user;
}
}

The application layer
This layer consists of DTOs, data mappers, and controllers. The application layer acts as the entry point to the domain service. In other words, it accepts the user requests and passes them to the domain layer.

In the case of AddService, the only kind of request that is accepted is to add a new user. The UserController offers the add() method for this purpose. It accepts a NewUser as the parameter and returns UserRecord. Both the NewUser and UserRecord are DTOs. Refer to Figure 4.

The application layer of AddService
Figure 4: The application layer of AddService

A DTO is just a data structure useful for carrying the data between the domain layer and the client. It does not carry any domain logic.

Following is the implementation of the application layer. Being a DTO, the NewUser is a simple data structure. The DTOs do not apply any business rules or logic. Hence it is OK to make the attributes public.

package com.glarimy.ums.app;

public class NewUser {
public String name;
public long phone;

public NewUser(String name, long phone) {
this.name = name;
this.phone = phone;
}

}

Like NewUser, the UserRecord is also a DTO. The implementation is self-explanatory:

package com.glarimy.ums.app;

import java.util.Date;

public class UserRecord {
public String name;
public long phone;
public Date since;

public UserRecord(String name, long phone, Date since) {
this.name = name;
this.phone = phone;
this.since = since;
}

}

And, finally, the UserController is the one that connects with the domain model to serve the user requests. It may also connect with the other infrastructure like message brokers, email servers, etc.

These kinds of classes are popularly called Controller classes largely due to the influence of the MVC pattern and Spring framework. For that matter, they can be named as you wish. However, these classes follow a simple process in serving the user requests.

Controllers map the input DTO to appropriate domain objects, invoke operations on domain objects, convert the results back into DTOs and return them to the caller.

Notice that the domain objects are never leaked beyond the application layer and the domain layer never entertains DTOs.

The UserController also follows the same pattern:

package com.glarimy.ums.app;

import com.glarimy.broker.Event;
import com.glarimy.broker.Publisher;
import com.glarimy.ums.domain.Name;
import com.glarimy.ums.domain.PhoneNumber;
import com.glarimy.ums.domain.User;
import com.glarimy.ums.domain.UserRepository;

public class UserController {
private UserRepository repo;

public UserController(UserRepository repo) {
this.repo = repo;
}

public UserRecord add(NewUser newUser) {
Name name = new Name(newUser.name);
PhoneNumber phoneNumber = new PhoneNumber(newUser.phone);
User user = new User(name, phoneNumber);

user = repo.save(user);

Event event = new Event(“”, “”);
Publisher publisher = new Publisher();
publisher.publish(event);

UserRecord record = new UserRecord(user.getName().getValue(), user.getPhone().getValue(), user.getSince());

return record;
}
}

The UserController is also supposed to handle the domain exceptions, though it is ignored in the above-illustrated code. Another point that can be noticed in the code is that it is also connected to BrokerService for firing the events for journaling.

That’s all! Any microservice of this nature consists of such domain and application layers, at a minimum. All the remaining technical stuff is handled by the infrastructure layer.

Infrastructure layer
This layer consists of databases, message brokers, directories, file servers, email servers…and the list just goes on. The infrastructure is not specific to a domain, which is obvious. It is more technical in nature and may be available as frameworks, libraries, etc, off the shelf.

In our case, AddService is using BrokerService and a set of Framework classes. Let’s briefly explore them as well.

The BrokerService: This is an infrastructure service that offers a way for publishers and subscribers to stay in touch with each other, asynchronously. Publishers publish events to the broker and subscribers listen for the events. A full-scale broker design is not covered at this point. Just its API classes are good enough for the publishers and subscribers like AddService. Normally, these kinds of infrastructure services are offered by third-party tools like Kafka. In our case, we are building these in-house.

Our BrokerService offers an API that consists of Event and Publisher, as presented in Figure 5.

Figure 5: The API of BrokerService
Figure 5: The API of BrokerService
package com.glarimy.broker;

public class Event {
private String topic;
private String message;

public Event(String topic, String message) {
//TODO: validation
this.topic = topic;
this.message = message;
}

public String getTopic() {
return topic;
}

public String getMessage() {
return message;
}

}

And the actual implementation of the publisher is not that important for us, at this point!

package com.glarimy.broker;

public class Publisher {
public void publish(Event event) {
//TODO
}
}

Generic framework: The AddService and many other microservices require some sort of framework to handle the common technical work. For instance, the framework may offer factories, builders, exception stations, proxies, decorators, and what not? Again, such frameworks are available in the market. In our case, we are building the framework as well. The simple model of our framework is presented in Figure 6.

The framework classes
Figure 6: The framework classes

Since the framework is generic, it belongs to the infrastructure and many microservices (not just AddService) may want to use it.

Thus we designed AddService as a microservice, and BrokerService and Framework as reusable infrastructure services.

FindService

Once you design a microservice, you can design any number of microservices. Just like in the case of AddService, the FindService also designs relevant domain models, application layers and uses some technical services that form the infrastructure layer.

Observe Figure 7. You will notice that the entities and value objects of the domain model are more or less similar to that of the AddService. This happens in any microservice. Yet, the teams may not even know about it as they work fairly independently on their own platforms. For example, while AddService is implemented on Java, FindService may be implemented on Python.

The domain model of FindService
Figure 7: The domain model of FindService

Figure 8 depicts the application layer of FindService. This also follows the same pattern as we have seen in the case of AddService.

The application layer of FindService
Figure 8: The application layer of FindService

Since we have seen the code for AddService, there is no point in illustrating the code for the remaining services. It saves both time and space.

SearchService

By now it must be clear that designing these kinds of microservices follows this pattern: 1) Domain model with entities, value objects and repositories, 2) Application layer with controllers and DTOs, and finally, 3) Infrastructure layer with a whole set of generic technical services.

Figure 9 presents the domain model of SearchService and Figure 10 depicts the application layer.

The domain model of SearchService
Figure 9: The domain model of SearchService

JournalService

And finally, the fourth microservice of the UMS is to listen to the message broker for the events and log them to a centralised journal. Being a separate microservice, this will be developed and deployed independently of the other services.

Figure 10: The application layer of SearchService
Figure 10: The application layer of SearchService

Refer to the previous parts of this series, for the way we designed journaling in monolith UMS. The design of JournalService has not changed much except for the fact that it is now an independent service. Figure 11 presents the domain model.

The domain model of JournalService
Figure 11: The domain model of JournalService

Perspective
We have just started our journey into the world of microservices. We have refactored the UMS as a collection of four microservices and presented their models. As mentioned at the beginning of this article, the services may be developed on a common platform or on different platforms. However, they are ultimately deployed on a microservices infrastructure.

What does that infrastructure look like? How about data management across the microservices? How do these services collaborate with each other? How are these services actually developed and deployed? What is the role of Dockers? What is the role of Kubernetes?

Well, we will solve these puzzles in the next several parts.

LEAVE A REPLY

Please enter your comment!
Please enter your name here