In the last few years, there has been a proliferation of articles related to microservices. Martin Fowler provided a helpful overview of the approach in a blog post in 2014. Historically, microservices seem similar to past recommendations to adopt a service-oriented architecture. Fundamentally, these approaches advocate breaking up a large monolithic application into a set of discrete services, each running in its own process and supporting an interface based on a lightweight communications protocol, like HTTP. These services usually run on an isolated server tier and can be deployed independently through automated processes. Each service normally supports a major functional area within an application, like search, fraud, messaging, etc.
In a previous article, I covered the challenges of maintaining a monolithic architecture. Assuming you have made the decision to migrate to microservices, this article will provide some tactics to consider.
In planning your move to microservices, here is an initial set of recommendations:
- Team. Designing back-end applications around a microservices approach works best if the team is organized in a similar way. This means dedicating a set of individuals to each microservice. As the number of microservices expands, you may find common groupings among them and assign the same team to own more than one microservice. The supporting team should consist of several engineers, a product manager and a QA tester.
- Start small. If your team is beginning its migration to microservices, you should start with a simple use case implemented as a single instance. Building and supporting microservices will represent a new paradigm for your team. Selecting a single function initially to migrate will reduce the impact of learning mistakes. Create the first service and let it bake for a few weeks before migrating more.
- Include common infrastructure components. For the first few services you build, you will want to validate your technology approach as part of a proof of concept. In order to make this proof of concept encompassing, ensure that your initial use case touches your major infrastructure components. These can include data storage, caching systems and message brokers.
- Technology selection. It’s usually best to maintain a single technology framework for your microservice implementation. This allows your team to develop expertise and share code across implementations. It also simplifies DevOps and issue resolution. If you find unique use cases that argue for more than one framework, at least try to maintain a minimal set. Long term support of your applications shouldn’t be impacted by a few employee departures.
Most internet-based businesses deliver their functionality through some set of client apps (web, mobile, desktop). The data and business logic required to make these apps function is usually provided by a single back-end application, hosted at a central location on the internet. Apps will communicate over a secure, open communication protocol like HTTP. The back-end application will generally expose its logic through a set of RESTful APIs. In most companies, the back-end starts as a monolithic application that runs on a set of web servers. Business logic and data access code for every supported function is included in this single monolithic application. Once you decide to break up this application and migrate functions to microservices, you will still want to maintain a lightweight entry-point application, with the same RESTful API structure. This API layer will handle the overhead of client app to server communications – authentication, routing of client requests, session management and packaging of responses. After parsing a request, the API layer will determine which back-end microservices are needed to fulfill it. It will dispatch remote calls to these services. Ideally, this is done in parallel, to reduce end-to-end response times. The calls to each microservice should include logic to process failures and kill the connection after a set time-out period. This failure logic is important to ensure that the overall response to the client is not hung on a single slow micro service.
For your microservices implementation, you have a wide array of choices, much like selecting any back-end server infrastructure. However, given that microservices are usually called as part of a response chain, rapid processing is important. Therefore, you should select a technology that is highly performant with strong data retrieval capabilities. Your choice boils down to a language and an associated framework with libraries that handle the overhead of processing HTTP requests/responses.
As you consider your choices, here are some guidelines to help. First, take your team’s experience into account. If your team has many members with expertise in a particular language, that should influence your decision. Ramp up will be lower. If not a particular language, then at least experience with a class of languages – interpreted versus compiled, functional, JVM based, etc. Second, your language and framework should support the components of your infrastructure, like your database technology, caching systems, any specialized functions like photo transcoding or statistical libraries. You wouldn’t want to select a language/framework and discover down the road that it has limited support for a critical use case. Finally, you should consider the future trajectory of the language/framework as part of your choice. Is there a lot of momentum behind it currently? Does it have comprehensive documentation and an active support forum?
As you consider your options, here are two popular choices for high scale microservice implementation:
- Go programming language. Introduced by Google in 2009, Go is designed to make developers highly productive. Its syntax is expressive and clean. It supports concurrency, which allows your back-end service to take full advantage of multicore servers. Go is statically typed and compiles to machine code, allowing it to run very fast. While being statically typed, it was designed to be very readable, without too many mandatory keywords and repetition (concise variable declaration, for example), making it look more like a dynamic language. It also has some interesting additions, like allowing functions to return multiple values. Concurrency support is implemented through goroutines and channels. The goroutine convention allows a function to be started with the go keyword, causing the function to be run in a separate operating system thread. This is ideal for remote operations, like retrieving data from a database, or background tasks. Channels provide support for sending messages between goroutines, allowing for synchronization and blocking. For frameworks, there are several that support a RESTful API implementation over HTTP. The space is continually evolving, but these frameworks have traction: Revel, Beego and Martini/Gin. As a technology choice for powering high-performance microservices, Go was recently mentioned on the Uber engineering blog.
- Java with Play. Java is a longtime choice for implementing back-end services. There is a large developer community behind it, making recruiting straightforward. Java version 8 adds many of the features popular in functional languages (like Scala), including support for lambda expressions, default methods and parallelism. The Play framework is built on top of Akka, which is an Actor-based runtime for building concurrent, distributed applications on top of the JVM. Akka is geared towards making applications reactive – highly responsive, resilient to failure, elastic and message-driven. Play provides a web application framework on top of Akka. It is stateless, supporting an asynchronous model with non-blocking I/O operations. It is also RESTful by default with full JSON support. Play supports implementation in both Scala and Java. The reason I advocate for Java is the lower developer learning curve. Recently, a few of the larger internet-based companies, including LinkedIn and Twitter, have expressed challenges with their migration to Scala and are moving some of that infrastructure back to Java.
Another advantage of a migration towards discrete microservices is testability. Given the encapsulation of a microservice, your team can create a set of tests for each one in isolation. Given that microservices typically do not involve a user interface, automation of testing should be straightforward. You could employ a test automation framework geared towards generating RESTful API requests and checking responses. Some options include Chakram and Frisby.js. Automated tests can be integrated into your CI environment, allowing a set of tests to be verified against every code change.
Ideally, coding against a microservice should be self-service for developers. This is best accomplished through documentation. If an application function needs to retrieve data from a particular microservice, the developer should be able to read about it in a shared documentation repository. Even better, developers can reference a catalog of available services and decide what they need to utilize. Microservice documentation should include the inputs that can be sent to the service and expected outputs. Exception handling should be addressed as well.