microservices antipatterns and pitfalls - Grains of Sand Pitfall

Perhaps one of the biggest challenges architects and developers face when creating applications using a microservices architecture is service granularity. How big should a service be? How small should it be? Choosing the right level of granularity for your services is critical to the success of any microservices effort. Service granularity can impact performance, robustness, reliability, change control, testability, and even deployment.

The grains of sand pitfall occurs when architects and developers create services that are too fined-grained. Wait—isn’t that why it’s called _micro_services in the first place? The word “micro” implies that a service should be very small, but how small is “small”?

One of the primary reasons this pitfall occurs is because developers often confuse a service with a class. Too many times I’ve seen development teams create services by thinking that the implementation class they’re writing is actually the service. Nothing could be further from the truth. 

A service should always be thought of as a service component. A service component is a component of the architecture that performs a specific function in the system. The service component should have a clear and concise roles and responsibility statement and have a well-defined set of operations. It is up to the developer to decide how the service component should be implemented and how many implementation classes are needed for the service.

As Figure 5-1 shows, a service component is implemented through one or more modules (e.g., Java classes). Implementing a service component using a one-to-one relationship between a module and a service component not only lends itself toward components that are too fine-grained, it also leads to poor programming practices as well. Services implemented through a single class tend to have classes that are too big and carry too much responsibility, making them hard to maintain and test.

500

Figure 5-1. Relationship between modules and a service

The number of implementation classes should not be a defining characteristic for determining the granularity of a service. Some services may only need a single class file to implement all of the business functionality, whereas others may need six or more classes.

If the number of implementation classes has no impact on the granularity of a service, then what does? Fortunately, there are three basic tests you can use to determine the right level of granularity for your services: the service scope and functionality, the need for database transactions, and finally the level of service choreography.

Analyzing Service Scope and Function

The first way to determine whether your services have the right level of granularity is to analyze the scope and function of the service. What does the service do? What are its operations? Documenting or verbally stating the service scope and function is a great way to determine if the service is doing too much. Using words like “and” and “in addition” is usually a good indicator that the service is probably doing too much.

Cohesion also plays a role with regards to the service scope and function. Cohesion is defined as the degree and manner to which the operations of the service are interrelated. You want to strive for strong cohesion within your services. For example, let’s say you have a customer service with the following operations:

  • add_customer
  • update_customer
  • get_customer
  • notify_customer
  • record_customer_comments
  • get_customer_comments

In this example the first three operations are interrelated as they all pertain to maintaining and retrieving customer information. However, the last three (notify_customer, record_customer_comments, and get_customer_comments) do not relate to basic CRUD operations on basic customer data. In analyzing the level of cohesion of the operations in this service, it becomes clear that the original service should perhaps be split into three separate services (customer maintenance, customer notification, and customer comments).

Figure 5-2 illustrates the point that, in general, when analyzing the service scope and function you will likely find that your services are too coarse-grained and you will move toward services that are more fine-grained.

500

Figure 5-2. Impact of analyzing service functionality and scope

Sam Newman offers some good solid advice in this area—start out more coarse-grained and move to fine-grained as you learn more about the service. Following this advice will help you get started in defining your service components without having to worry so much about the granularity right away.

While analyzing the service scope and functionality is a good start, you don’t want to stop there. After looking at the service scope, you need to then analyze your database transaction needs.

Analyzing Database Transactions

Another test for validating the level of service granularity is the need for database transactions for certain operations. Database transactions are more formally referred to as ACID transactions (atomicity, consistency, isolation, and durability). ACID transactions coordinate multiple database updates into a single unit of work. The database updates are either committed as a whole unit or rolled back if an error condition occurs.

Because services in a microservices architecture are distributed and deployed as separate applications, it is extremely difficult to maintain an ACID transaction between two or more remote services. For this reason, microservices architectures generally rely on a technique known as BASE transactions (basic availability, soft state, and eventual consistency). Regardless, there will usually be times where you do require an ACID transaction for certain business operations. If you find you are constantly battling issues surrounding ACID vs. BASE transactions and you need to coordinate multiple updates, chances are you have made your services too fine-grained.

When analyzing your transaction needs and find that you can’t live with eventual consistency you will generally move from fine-grained services to more coarse-grained ones, thereby keeping multiple updates coordinated within a single service context, as illustrated in Figure 5-3.

500

Figure 5-3. Impact of analyzing database transactions

Notice that from an ACID transaction standpoint it doesn’t matter whether you consolidate the separate databases or keep them as individual ones. Generally you will want to consolidate the databases as well, but this is not a requirement to maintain an ACID transaction (assuming the databases and the transaction manager you are using support XA—e.g., two-phase commit—transactions).

Once you have analyzed your transaction needs, it’s time to move on to the third test, service choreography.

Analyzing Service Choreography

A third test you can use to validate the level of service granularity is service choreography. Service choreography refers to the communication between services, also commonly referred to as inter-service communication. Service choreography is generally something you want to be careful of within in a microservices architecture. First of all, it decreases the overall performance of your application since each call to another service is a remote call. For example, assuming it takes 100 milliseconds to make a restful call to another service, making five remote service calls is a half a second spent just in remote access time.

The other issue with too much service choreography is that it can impact the overall reliability and robustness of your system. The more remote calls you make for a single business request, the better the chances are that one of those remote calls will fail or time out.

If you find you are having to communicate with too many services to complete single business requests, then you’ve probably made your services too fine-grained. When analyzing the level of service choreography, you will generally move from fine-grained services to ones that are more coarse-grained, as illustrated in Figure 5-4.

500

Figure 5-4. Impact of analyzing service choreography

By consolidating services and moving to more coarse-grained services you can improve performance and increase the overall reliability and robustness of your applications. You also remove dependencies between services, allowing for better change control, testing, and deployment.

The other approach when dealing with service choreography to help overcome the performance and reliability issues is to leverage asynchronous parallel processing combined with reactive architecture techniques for error handling. Executing multiple requests at the same time increases overall responsiveness, allowing you to coordinate multiple services in a single business request in a timely fashion. The key point here is to understand and analyze the trade-offs associated with service choreography to ensure both sufficient responsiveness to the user and sufficient overall reliability of your system.