The era of monolithic applications with one big database is long gone. These applications are terrible in every aspect of the application lifecycle management. Especially in production operations and continuous maintenance & improvement.
If you are about to start a new cloud native project, it has to be microservices. There are a number of benefits of adopting microservices architecture. The most important ones are:
- from the software development point of view - you will decompose your application into smaller services which follow single responsibility pattern (reduced complexity & clear design); are faster to develop as different teams can work on different services simultaneously and release them to production more frequently (and independently of others);
- from the production operations point of view - your application will be fine-grained and thus have a better availability, resiliency, and performance.
You probably seen videos from Kubeconf, AWS, Azure, Google conferences where presenters talk about hundreds of services or show dependency graphs in 10% scale so that they can fit on a slide.
For most of us this is going from microservices to nanoservices.
I do have to admit that there are cases where nanoservices are the only way of making your application work. A single function becomes a service of its own (Function as a Service). Such function is then scaled independently of other functions/services. These are the applications that process billions of events an hour. These types of applications are a totally different universe and only a few lucky ones get the chance to work with them.
So, unless you absolutely have to go nanoservices route I recommend not to do it. You risk losing all the benefits of microservices. You will spend more time on managing your services (some people call it "loving your microservices"), orchestrating releases, figuring out the dependencies between them, and trying to keep track of the costs.
When you're starting architecting your application you can start with just a few services. Break your application into functionally independent and loosely coupled services. At the design stage think of what are the potential candidates for splitting your application further. If you make these decisions (or at least think about them) at the design stage, you will be well prepared to refactor your application in the future.
OK. Let's see 2 examples of architecting microservices.
We are developing a HR cloud native system. How to architect it as a microservices cloud native app?
- the front-end application is a single page application; the application is served using CDN (note: some CDNs also support serving dynamic content or even handling PUT/POST/DELETE requests which is super useful and helps reduce latency even further);
- all user requests go through the API gateway;
- API gateway connects to authentication service to validate requests and passes through only the valid ones;
- API gateway routes traffic to one of the functional microservices where the actual data processing and data storing happens;
- functional services can be: personal information management, recruitment, time-tracking, benefits, career development, etc. (you get the drill); each of the functional service can be scaled independently (probably personal information management and time-tracking are the most popular ones);
- each functional service has its own database; the database is private to its service;
- services can talk to each other only via API (sync) or event bus (async);
- long running processes are ran by background services; these services are broken down into functional areas too; in case of our HR system this could be reporting, payroll integrations, etc.;
- you must have an audit service (seriously for every cloud native app you must have it); all actions performed in the system should be recorded; there are regulations like EU's GDPR or California's CCPA and there are customers dying to ingest your audit events for their own auditing and governance & compliance purposes;
- audit service should be externally accessible to your customers so that it can be integrated with customers' SIEM solutions;
A little bit tricker scenario, but also quite popular. We have an existing application. To make things worse it's a monolith application. Can we make it microservices? Yes, we can.
Say we have an application which supports URI paths like
/invoices. They take you to different parts of your application where you can list, view, create, update, and delete resources.
Splitting the code is easy. A high-level action plan looks like this:
- create 3 new services by creating 3 exact copies of the existing codebase;
- at the application load balancer level create 3 routing rules (
/invoices) and point them to the newly created services;
- release it;
- remove the logic that does not belong to given service; from employees service remove code related to companies and invoices, perform similar exercise for companies and invoices services;
- release it;
- any common code left should be extracted to a shared library and referenced as a dependency;
- release it.
Great! We now have 3 services. However, they all talk to the same SQL database. Breaking monolith database is not as easy, but it's still not a rocket science. A high-level action plan looks like this:
- create a new database config for your every service; at first they all point to the same physical database;
- release it;
- list all foreign keys that cross service boundaries; a classic example is user entity which usually ends up on many other entities; hopefully there shouldn’t be too much of them;
- review the database access logic to catch all the places where you need to sync data in more than one service; this is an important step because from now on you will be responsible for managing the integrity of your data; in theory there should only be scalar foreign keys (which will become ordinary int/bigint/UUID columns now); also the foreign keys (which are primary keys in their original tables) never really change so there is no need to update foreign keys when updating the original entity;
- release it;
- drop the FK constraints; once you complete this step you will logically split the database schema;
- release it;
- split the data access layer into separate libraries; group data access logic by the service functionality; refactor common code to a shared library;
- release it;
- next move is to physically split the database; you can do this by: using fork functionality, restoring from snapshot, restoring from point-in-time, or first creating read replicas and then promoting them - choose the approach that is the best for you; remember that based on your split strategy and the size of the database this can take some time;
- update database configs to point to new databases;
- release it;
- the last action you need to do is (similar to what we did with the code) remove the database components that don't belong to given service (like remove all invoices and companies components from employees database and vice-versa).
Release often and in small steps to reduce the risk and gain confidence in your plan.
Now that we know how to build microservices we need to actually deploy them.
It should come as no surprise that for microservices I would recommend Kubernetes and serverless for nanoservices.
With Kubernetes you can easily port existing applications. I would say that every application that was written in the past 15 years can be containerized. Every major cloud provider apart of having their own managed Kubernetes service, can help you move your existing applications to the cloud: AWS App2Container, Azure Whitepaper: Containerize your apps with Docker and Kubernetes, Google Migrate Anthos.
In short: if you didn't hardcode configuration inside your code nor didn't make anything silly, you are good to be containerized.