We are going to divide this subject into two articles
Creating a monolith that easily scales to microservices
Moving a monolith that had no prep for scale to microservices (article 2)
Our goal is avoiding the WAD in the future and having to untangle a mess
There is much hype around microservices and having myself worked at Twitter in a monorepo of hundreds of microservices, yes, it was amazing. In fact, why would you want to even think of using a Monolith and when? Let's talk about the cost of building 5 microservices instead of a monolith at a startup. First, let's look at extra development needed for microservices
CI/CD is not just a single server but 5 now
Metrics and monitoring has to be done 5 * 15 for about 15 notifications per server
Paging has to be setup for 5 servers in case they go down
From my blog at Twitter, A more involved error handling strategy has to be designed
You must design and implement a way to trace every request
You need to create&configure 15 servers (3 environments * 5 servers)
You must create clients(api to protocol) and scaffolding (protocol to controller)
These initial startup costs of creating a microservice architecture are not cheap and are well worth if for someone who has product market fit like Twitter and Amazon. Until then, if you are a startup without market fit, you need to move much faster and get product market fit first. This begs the question
How can you build a monolith that scales into microservices?
There are a few rules I use here. Here is the overview of rules but they are too complex to understand with just reading the bullets so I will go into detail with examples
Creating APIs that are asynchronous and you can extract into a microservice later
No database transactions in the http filters
Pay attention to your local and remote exception design early
Pay heavy attention to testing and be prepared for feature testing early
Creating a build system to prevent 'the wad'
First, here is an example of an API that can go remote and be async. In java, we use XFuture as CompletableFuture is missing a way to pass through request context(another more advanced topic for later and critical for user transaction logging). Let's look at the method in java and typescript
public XFuture<FetchValueResponse> fetchValue(FetchValueRequest request);
function fetchValue(request: FetchValueRequest): Promise<FetchValueResponse>;
These methods are completely asynchronous methods such that the code calling them will not care if the code behind the API is remote or not. The next thing is the exception strategy must be such that your code behind these apis can throw exceptions and when you move to microservices later, the exceptions thrown must be 1 to 1 with the design. Make sure to note all the noteworthy categories of exceptions like my blog article on Twitter . You can go over the wire with codes or in some things like Thrift and gRPC, it will do this for you. To be as close as 1 to 1 with things like gRPC and Thrift, you can start with JSON but should read when people intentionally violate REST. If you decide to go from monolith to microservices with json as your first step, then each request and response DTO defines the JSON for requests and responses but what about errors? We use a simple structure here
"message": "Exception from server unless this api is used by customers ;)",
Scaffolding is the code that wires the json/http protocol to the controller. You generally want a single scaffolding that is re-used for every microservice. Scaffolding is missing on most platforms but can be recreated or made easier for developers to do. As a concrete example of scaffolding, look at lines 67 and 68 in webpiecesexample
new RESTApiRoutes(SaveApi.class, JsonController.class),
new RESTApiRoutes(ExampleRestAPI.class, JsonRESTController.class),
and next we look at SaveApi
public XFuture<SearchResponse> search(SearchRequestrequest);
So, the scaffolding is RESTApiRoutes class which will map an incoming POST request to path "/search/item" to the controller "JSONController" and method "search". If you are using gRPC or Thrift, this scaffolding is done for you. In JSON, most platforms miss doing this for developers unfortunately though startups could really use it. This means developers using the webpieces platform do not deal in protocols anymore and only deal with APIs and contracts. In fact, Orderly Health (a user of webpieces) can swap protocols to gRPC without affecting any developers. Developers are independent of the protocol chosen by the platform team.
Last and the hardest piece comes database transactions. You will really need to put database transactions behind each API call so when you move the code behind the API into it's own microservice, you also move the transaction along with it. The best thing to do in this case is create something like Transactionhelper so you can simply inject it and call txHelper.runCodeInTransaction(Function function). In this way, you can easily have each developer do transactions in the libraries behind the APIs.
Testing is critical and it is best to test from your APIs to your simulate APIs so that as you refactor code, you do not lose your test suite costing anywhere from 30k to 150k on previous projects I have seen(sometimes more). You may start with just 1-3 apis in your monolith and you should test from API to API so when you pull them apart, the test suite moves with your library that converts to a microservice.
Avoid the wad
Next is your build. Make sure that your implementations behind these APIs do not depend on any main code or have fun untangling the mess
The Final Move
Now you are ready, and you have a live running system. Embed the library in a microservice and launch an instance that gets 0 traffic to start with. It is hitting the same database so now it is only a matter of redirecting traffic in the monolith from the library to the new microservice using the same exact API.