Large and complex software applications with tens of thousands of concurrent users need an architecture that allows your development team to make frequent small releases without the risk of crashing the entire application when a bug is introduced. Test-driven development (TDD), continuous delivery tools like Jenkins, and a microservices architecture work together to allow development teams to make frequent updates to your applications.
A microservices architecture helps agile teams move fast and ensures your entire application doesn’t go down when a critical bug is introduced. Over the last couple of years, we’ve helped enterprises across automotive, travel, construction, retail, and healthcare industries transform monolith and SOA applications into a set of loosely coupled services. While improving the application architecture for companies like Katerra, Volkswagen, and Walmart was exciting, there are many challenges large firms face when transitioning to a microservices architecture.
Many startups and fortune 500 companies build new software products using a monolithic architecture. When you want to bring a product to market quickly, a monolithic architecture allows your development team to iterate fast when the feature set, code base, and a number of simultaneous users is small. As a new product gains traction, it often makes sense to transition to an architecture that allows different systems and features to be deployed independently from each other.
The benefits of a microservices architecture are clear:
Increase development velocity - Your organization becomes more agile in responding to new market requirements by adopting this architecture, allowing you to roll out robust digital capabilities easily and quickly to employees, customers and partners alike.
Reduce downtime - Microservices add reliability to your IT infrastructure, and they are faster to build, deploy and test. The failure of a single module doesn't affect the entire application.
Increase the pool of talent you can hire - You don’t have to reject developers who aren’t familiar with the languages or frameworks your application is written in. Microservices offer greater productivity and flexibility, allowing your developers to code in the languages and frameworks they’re most comfortable with and independently develop services that can be consumed by any team within your organization.
Eliminate bottlenecks and increase scalability - It’s easy to scale your entire application once you make the transition to a microservices architecture. By breaking your monolith into a smaller set of independent services, it's easier to manage, scale, and extend your application. Scalability is one of the major driving forces behind any kind of architectural change.
A microservices architecture breaks each of your applications into smaller microservices that talk to each other over the network. When adopting microservices, a major challenge you’ll face is learning how to mix other architectural patterns already deployed with this pattern. You need to balance the flexibility and speed that microservices fuel with the complexity they create. For instance, as soon as you break your monolith down into services, you are introducing interfaces between each of them. Both endpoints need to exchange the same message formats and retain the same semantic understanding of those messages. This means a simple change to one microservice may require changes to several other microservices with dependencies on the microservice being changed. Your DevOps team will need to coordinate the release of these changes, which causes operational complexity. Many others have written entire articles about the complexity of microservices, and we won't get into all the details here.
Step 1 - Audit & Plan
1. How well designed is your current web services architecture?
This answer will define how easy the journey will be for you. A well-designed monolith application with good code coverage can be broken down into microservices with considerably less effort. For decomposing large systems into smaller microservices, the domain driven design (DDD) approach to modularizing applications based on your business context is ideal. Here you define independent areas of problems as bounded contexts. The DDD approach advocates separating the monolith into multiple standalone services or with the help of bounded contexts, developing them separately from the start.
2. How good is your documentation?
Some large companies either don’t have proper documentation for their existing web services or the documentation is incorrect. In order to find and fill in the gaps, your development partner needs to test each of your existing APIs to ensure they’re getting the expected responses. Knowledge transfer meetings with key members of your development team who own different parts of your backend architecture help them understand how your systems work.
3. Which systems need to share a database?
Ideally, no two microservices should have to share a database so that they can scale independently. But, if they have to share a database, no other microservice should be able to directly modify the data stored within the database of another microservice. When you are adopting a microservices architecture, you need to decouple your applications/services from a single shared database. Design your microservices architecture so that each individual microservice has its own separate database with its own domain data. This allows you to independently deploy and scale your microservices, and your individual teams can own the databases for the microservices they develop.
4. Which systems should be decoupled?
Begin by decoupling your simple edge services, then decouple capabilities deeply embedded in your monolithic system. When you start the journey, your most significant risk is that your delivery team fails to operate your microservices properly. This is why it's advised to practice the operational prerequisites by focusing on edge services first. Your developers can then split the monolith once they have successfully migrated the edge services. You need to decouple the data architecture to realize the full benefits of Decentralized Data Management.
5. How to phase the development of microservices?
Microservices projects are never the same. No matter how much experience you have, there are always aspects of each project that are very unique to your product. One thing that microservices projects have in common with standard projects is there will be nice-to-have and must-have features, which need to be prioritized throughout the project. Below are some broad phases most microservices projects follow:
Shift and lift - First, shift and lift the monolithic application into a cloud environment. Organizations can take advantage of cloud-native capabilities in this initial phase to improve data durability, availability and security. This phase also allows for a transition to a more flexible on-demand model, reducing hardware management requirements and simplifying license management.
Containerize - Evolve your application code base to provide greater modularity and flexibility. To make your architecture more flexible and take full advantage of cloud-native features, address specific items within the code. Then, evaluate the code and work towards converting the code base into a container-friendly platform.
Scale - Scale the container image into a production environment across various instances in the cloud. Your server application will now work on any operating system or cloud hosting provider like AWS, Google Cloud or Microsoft Azure.
Evolve - Now your IT organization can begin to build a DevOps model with application development evolving into microservices patterns. To evolve the development environment into a DevOps culture, bringing with it faster deployment, agility, and user-centric delivery, commit to the phases described above.
Step 2 - Architect & Develop
1. Design your microservices
Design first approach- Like many companies that have made the transition, you may want to adopt a Design First approach to building microservices. Here you choose to define and design the interface of the microservice first, stabilizing and reviewing the architecture before implementing anything.
Test to ensure code is operating properly - With monolithic application architectures, the design issue is that there’s so much code implementing different features. You will have to coordinate across different groups to ensure all code continues to operate properly before making changes to your monolithic app.
Define the functionality of each element - Start by defining what the first element of a microservice should do. What is the breadth of functionality you expect implemented?
Partition - Each service must have a small set of responsibilities. You may be concerned you’ll over partition functionality in your initial foray into microservices and end up with too many microservices to manage. In our experience, over-partitioning is rarely an issue as development teams often stuff too much into each service.
Define the scope - Partitioning services along logical functional lines is one way to define the proper scope. Mirroring the development organization’s structure is another scoping approach. Minimizing a service to the amount of code that can be re-implemented by your team in a certain week period is another approach. You’ll avoid the problem of bloated services by rationing the size of each microservice in this fashion.
2. How to develop your microservices
So, how do you transform monoliths into microservices? One way to get there is by effectively structuring your application as though it’s completely external, without removing the code from it. First, you need to differentiate the actions which can be performed and the data that is present as outputs and inputs of those actions.
Once you’ve defined two distinct classes where one class performs the operations and another one models the data, you can call the same actions over and over, and expect consistent results. Your otherwise monolithic application will continue to operate as normal over the network. To transform your existing code into a scalable external service, group these classes into a library and substitute the network client with the previous implementation.
This is a Hexagonal Architecture, where the external components are orchestrated around the core of your application to achieve your goals, and the coordination is in the center. Here, the capabilities related to a specific business domain are insulated from any outside effects or changes.
3. How to handle microservices authentication?
The whole application is a process in the monolithic architecture. To implement user authentication and authorization in the application, a security module is generally used. The security module of the app authenticates the user identity when the user logs in. A session is created, and a unique session ID is associated with the user after verifying the user is legitimate.
A session stores login user information such as Role, Username, and Permission. The server then returns the session ID to the client. The client sends the session ID as a cookie after recording it, in subsequent requests, to the application.
To verify the user’s identity, the application can then use the session ID without having to enter a password and username for authentication each time. Along with the HTTPS request, the session ID is sent to the application when the client accesses the application.
Through an authorization interceptor, the security module generally processes all received client requests. First, this interceptor determines whether the session ID exists. It knows whether the user has logged in if the session ID exists. It is determined if the user can execute the request or not by querying the user rights at that time.
4. How to handle authentication between microservices?
As far as authorization goes, the industry standard is usually OAuth/OAuth2. It's a widely adopted standard even though OAuth2 isn't perfect. Here you can rely on platforms and libraries that will greatly accelerate your development phase. The best solution is to make an entry layer for your request, which authorizes and authenticates the request. You can use headers or payloads if you need to share information between services.
It’s critical that the services are not accessible without going through the Auth Layer. Most microservice setups work this way. Why authenticate, if the User doesn't call a service directly? Let the auth layer decide who can enter and who can not once your application is in a closed network. If you need to handle authentication between microservices any other way, use JWTs (JSON Web Tokens). They are tokens that are used to authenticate users on applications. They’re safe, can store information and do just about everything you need. Use one call per service to generate the tokens. All other services just request the token.
5. How to handle microservices security?
When seeking to secure networking vulnerabilities, microservices represent the best of both worlds since they expose networking capabilities deep inside your application. You can secure your application at the microservice level, preventing attacks from spreading much sooner than is possible with other architectures. Because microservices networking is more dynamic and complex, traditional network security tools are rendered inadequate. Managed access through APIs ensures the security of microservices. Overzealous security practices lead to performance bottlenecks as you needlessly block the sharing of microservices among applications.
6. How to test microservices?
Microservices can function independently and together with other loosely coupled services. So, you'll have to test every component together and in isolation. High test coverage is met with a combination of testing strategies.
End-to-end microservice testing - This kind of testing is hard, and it gets a lot harder with each new service you introduce.
Unit testing - It’s straightforward to write unit tests for microservices. While a microservice may be small by definition, your unit tests can get even more granular. When your developers write unit tests, they exercise the tiniest piece of testable software in the application to see if it behaves as it should.
Integration testing - Unit testing alone can't guarantee the system will behave as expected. With integration tests, you’re testing the interactions between components and communication paths to detect issues. An integration test usually tests the interactions between external services, like another microservice or datastore and the microservice.
Component testing - Here you’re testing the microservice itself in isolation. An application is composed of a no. of microservices, so you need to mock the other microservices to test in isolation. Component tests also test the interaction of the microservice with its dependencies such as another database.
Step 3 - Monitoring Microservices
Collect statistics - Collecting statistics for load and performance will allow for comparisons between the microservices-based application and the monolith. This will give you better visibility into the gains that the new implementation brings to the system, and give you the confidence to pursue further migration.
Undertake health checks - Gathering data from platforms and applications is a passive form of monitoring. As a proactive means of regularly testing a service, use “health checks.” A health check can be more sophisticated than merely returning a fixed string. By executing a part of the logic used to process “real” work, these types of end-to-end checks test parts of the logic more thoroughly.
Monitoring tools and technology come in the form of platforms and libraries. Some tools include both, a library to instrument code and a platform to collect data.
Monitoring platforms - These focus largely on the analysis and gathering of the data that is collected from the operating systems, applications and network platforms on which they run. Prometheus is the one solution built with microservices in mind. Other monitoring solutions include InfluxDB and statsd.
Libraries - During development, libraries are incorporated into your application. Most popular language frameworks include resources for writing to data streams over a network, such as Ruby, Node, Python, .NET, Java, and Go. These resources can be used for monitoring and logging data.
Many companies, without ever labeling their practice, have been following an approach toward leveraging APIs that can be classified as microservices. The ideas mentioned in this whitepaper have been practiced and delivered results for large enterprises.