Consul
Model a Monolith as a Set of Microservices
Consul service mesh provides a robust set of features that enable you to migrate from monolith to microservices in a safe, predictable and controlled manner.
This tutorial focuses on modeling your monolith as a set of microservices. By modeling your monolith as a set of microservices, you will be more effectively informed when planning how best to launch a microservices pilot project. This tutorial is designed for practitioners who will be responsible for developing microservices that will be deployed to Consul service mesh running on Kubernetes. However, the concepts discussed should be useful for practitioners developing microservices that will be deployed to any environment.
In this tutorial you will:
- Review the high level planning process
- Review the example monolithic application
- Identify a good candidate service for extraction
If you are already familiar with planning and modeling a microservices pilot feel free to skip ahead to the next tutorial in the collection which discusses how to Extract a Microservice and prepare for deployment to the Consul service mesh.
Source code
This tutorials in this collection reference an example application. You can review the source code for the different tiers of the application on github.
Planning the project
To create a new microservice you have to answer several questions, and perform quite a few tasks, some of which are outlined below.
Decisions
- What language will you use?
- Will you use a microservice framework?
- How will you deploy the service?
- Will it be hosted in the same place as the monolith?
- Will you attempt to make this service a pattern for future implementations or a one off experiment?
- How will you handle source control and versioning?
Tasks
- Model what your monolith might look like as a set of independent services
- Assess each service for:
- Coupling & Dependencies
- Complexity
- Quality
- Deployability
- Testability
- Size
- How it creates value as a microservice
- Based on the assessment, identify which service to extract first
- Document the changes you will have to make so that the code can run as an independent microservice
- Document the changes you will have to make to the monolith once the microservice is extracted
- Design your microservice and document its API
- Create a test plan for the microservice
- Extract the code to a separate, deployable unit
- Figure out your deployment strategy
This is by no means an exhaustive list, but is a valid starting point for understanding the planning process. In this tutorial, you will work through this list as if you were implementing this project plan.
Monolithic application overview
The example application is called HashiCups, and it is an online ordering application for a fictional coffee company. Since HashiCups was developed relatively recently, it currently primarily separates concerns between the UI, API, and data layers with some separation of concerns for business logic.
The application has a React UI that can be deployed independently since it is
stateless, and consists of a set of static files that just need to be served
once per user session. When a user starts a new session the application is downloaded
in its entirety and runs in the user's browser from that point forward. In
javascript terminology, it is a Single Page Application or SPA. While there are
no server side templates that need to be rendered, the SPA is designed to
interact with an API that is served from the same domain at the /api
path prefix
or route.
Since the application is a mobile ordering app, the UI team decided to use a GraphQL server for their API layer. They liked the flexibility this offered across a lot of vectors, and are happy with their decisions. The GraphQL API could call any number of backend services, but right now, it only calls one: the Product API.
The Product API was originally envisioned as a thin REST API layer on top of a Postgres database that holds the product data. During the development of the initial prototype, that was what it was, but once the project got approved to go from Proof of Concept (POC) to Minimum Viable Product (MVP), decisions were intentionally made to increase speed to market.
The team was small, and the most important value the business held was getting the product released. The development team was also the operations team at the time and they felt that incurring some manageable technical debt was a reasonable tradeoff for speed. Like many projects, they realized that the product needed to prove that it added business value and had actual scalability concerns before it was worth investing in a complex architecture.
Once the product team defined the MVP they realized they were going to need to
address three areas of the business domain in order to launch: serving product
data, authentication and authorization,
and accepting orders. So the development team chose to just add some auth
handlers and orders
handlers to the Product API and some user
and orders
tables to the Postgres database and go to market.
So they did, and it worked! HashiCups is now a successful company. So successful that they are starting to have scalability issues with their current design. Also, the team has grown and deployment velocity is beginning to slow down. With a small team, having a limited number of deployable units of code was a benefit. They went live with just the UI, the GraphQL API, the Product API, and the Postgres DB.
Motivations for splitting up the monolith (optional)
Now, however, having products, auth{n|z}, and ordering all in the same project is becoming an impediment to progress. At some point, the team started referring to the Product API as the REST API, since its purpose has changed. Unfortunately, the code artifact names haven't changed and this has caused confusion when onboarding new team members.
Also, it's clear to everyone in the development team that the REST API does too many things. Unit tests are taking longer to run and the auth code is beginning to get too intertwined with the ordering code since ordering requires users to be logged in. Also, the product management team is getting frustrated because they want to run experiments with the UI and different product offerings more rapidly, but the development team doesn't feel confident release updates to the REST API that quickly because changes to the ordering sections of the code take longer to implement and test. They don't feel comfortable pushing the ordering team any faster, and they don't want long lived feature branches because that keeps causing them integration troubles when the code gets merged.
In this tutorial, the Product API will be considered the monolith that needs to be split up. The following diagram illustrates how all the domain logic for HashiCups is deployed as a single monolith. The lock icon below the Products block indicates that all the elements below are either part of or dependent on the auth{n|z} process.
Identify the first candidate microservice
Using the REST API as a starting point, its time to work through the decisions and tasks sections of the planning process outlined above.
Decisions matrix
Referring to the list of decisions outlined above, the team used a table to track each decision point, and the decisions they made.
Decision Point | Decision |
---|---|
Language Choice | golang |
Framework | won't use to start; locks in too many decisions; requires extra refactoring |
Deployment model | Manual kubectl to start |
Hosting | Same as monolith |
Pattern or Experiment | Experiment |
Source control/versioning | Separate git repo versioned with tags/release |
Service modeling and assessment
Now that they had thought through some of the high level decisions about how they would scope, manage, and implement your first microservice pilot project, the team needed to begin conceptually breaking apart the monolith into a set of potential microservices, so that they could decide which one made sense to tackle during a pilot project. One way to do this is to think about the monolith in terms of the different areas of the domain model, and they decided to take that approach.
Referring back to the go-to-market strategy, the business team had already identified that the solution needed to handle products, auth{n|z}, and ordering. One thing the team didn't realize when they got started was that payments really are a separate concern. They naively thought of payments as part of ordering, and had now embedded that logic into the ordering code artifacts. That mistake was a big part of their motivation for moving to microservices. They wanted to be able to experiment with different payment processors for multiple reasons. Also, the ordering module had the highest defect rate of any module in the system. That had everyone concerned because any defect that affected payments affected the bottom line. Stakeholders across the entire organization wanted to split payments from ordering and get a clearer perspective on what their actual quality position is. Unfortunately, that also seemed like the most difficult challenge, and the development team didn't feel like it would provide the quick win they needed for the pilot project.
The development team had a discussion, and realized they needed a way to quantify the effort relative to the value for each candidate microservice so that the other stakeholders had something to use as a basis for casting their vote on which service to address first. The tech team also realized that the current code quality would have an impact on the effort required to migrate the code. The better the code quality, the easier the refactoring job, and vice versa.
Up to this point, the team had used the T-Shirt sizing approach to effort and value estimation during sprint planning. Since the other stakeholders were familiar with this technique, it seemed like using a familiar tool would be welcomed by all, so they decided to use t-shirt sizes to estimate effort and value. Assessing code quality was not something the development team wanted spend a lot of time negotiating with non-developers. To keep things simple for all stakeholders, they settled on assessing code quality with a poor-fair-good designation. The other teams were happy to move forward with that approach, because it allowed them to provide their input based on defect rate rather than design of the implementation, and still gave them the ability to contribute to the quality designation.
Now that they had identified all the factors they would use to derive a score, they needed a way to turn these subjective designations into numbers. The team had been using fibonacci numbers for story point estimation up until now, and they were amazed at how well that technique seemed to work. So they came up with the following scoring matrix based on the fibonacci sequence that allowed them to derive a numerical score for each service based on effort - quality, and then compare that to the value score. In the end, while the designations were subjective, this approach gave them a way to quantify their own subjectivity!
- T-Shirt size to fibonnaci numbers (effort & value)
- 3: small
- 5: medium
- 8: large
- 13: x-large
- 21: xx-large
- Poor-fair-good to fibonnaci numbers (current code quality)
- 3: poor
- 5: fair
- 8: good
Notice that they chose to start a little higher in the fibonnaci sequence. This was so that the derived scores would have greater variance. This also avoided creating an impression that a low numerical score implied the effort would be small.
Finally, they realized that adding some level of granularity around the effort scoring would be necessary. They had to break the problem down to different concerns in order to come up with a meaningful assessment. They chose to divide their effort assessment into five different line items:
- Coupling & Dependencies
- Complexity
- Deployability
- Testability
- Size
These are the assessments they produced for each candidate microservice after applying the process described above.
Product
Concern | Analysis | Effort | Score |
---|---|---|---|
Coupling & Dependencies | upstream from ordering downstream from Postgres tightly coupled to Postgres | Medium | 5 |
Complexity | straight forward query service | Small | 3 |
Deployability | lock step with Postgres, but well understood | Medium | 5 |
Testability | Complicated by Postgres dependency | Medium | 5 |
Size | not much code | Small | 3 |
Effort score | 21 | ||
Quality | well written, but could use some query optimization | Good | -8 |
Effort minus quality | 13 | ||
Value score | makes it easier to move to data caching queries no longer competing with auth and payments processes | Large | 8 |
Effort/Value ratio | 13/8 | 1.625 |
Auth
Concern | Analysis | Effort | Score |
---|---|---|---|
Coupling & Dependencies | upstream from ordering currently shares code with orders tightly coupled to ordering | Medium | 5 |
Complexity | complicated by default coupling with ordering would need to be broken would like to replace with a managed service no analysis done on managed service options or impact if maintained internally, needs a separate database | X-Large | 13 |
Deployability | could be deployed indpendently requires new, separate database | Large | 8 |
Testability | requires automated pen testing on every build | X-Large | 13 |
Size | larger than we care to admit | X-large | 13 |
Effort score | 52 | ||
Quality | good enough, but could be better | Fair | -5 |
Effort minus quality | 47 | ||
Value score | not related to core business domain large amount of liability | Small | 3 |
Effort/Value ratio | 47/3 | 15.7 |
Ordering
Concern | Analysis | Effort | Score |
---|---|---|---|
Coupling & Dependencies | downstream from products downstream from auth currently shares code with auth tightly coupled to auth | X-Large | 13 |
Complexity | significant amount of domain logic payments should ideally be extracted to their own service coupling with auth would need to be broken needs a separate database would need to be able to validate orders against products | XX-Large | 21 |
Deployability | would now need to deploy two services would need to be able to authenticate with monolith would need to be able to call payment processor | XX-Large | 21 |
Testability | would require mocking all monolith dependencies uncertain how two services complicates things | XX-Large | 21 |
Size | more lines of code than the other two combined | XX-large | 21 |
Effort score | 97 | ||
Quality | highest defect count in the ticketing system | Fair | -5 |
Effort minus quality | 92 | ||
Value score | most likely to reduce overall quality and stability over time | XX-Large | 21 |
Effort/Value ratio | 92/21 | 4.4 |
Making the decision
After completing the analysis phase, the development team had a strong preference for extracting the Products service first. They believed it had the healthiest ratio of effort to value. However, not everyone on the business side of the organization even wanted to migrate to a microservices architecture. They were skeptical of the business value, and wanted to prioritize adding more features instead. The development team knew that putting off paying down this technical debt was only going to increase the overall complexity and therefore cost of the migration when it did happen. Also, they knew that the first experiment needed to be perceived as a success in order for the rest of the effort to get buy in from the business team.
Fortunately, by going through this planning process the development team was equipped with several data points they could use to explain their position in terms of business value.
First, by this point in time the business team understood the T-Shirt sizing approach to estimation, and had their own battle scars related to tackling initiatives larger than medium. They either already knew or had learned the wisdom of breaking efforts down into smaller increments.
Second, by pointing out the unknown aspects of each extraction effort, they were able to point out risk. The business team didn't understand a lot of the technical considerations of the project, but they were very capable managers, and they absolutely understood the risk of the unknown.
Third, by explaining the problems with the ordering code and then relating that to defect count in the ticketing system, they were able to express their argument for migrating to microservices in general using a data point that held value to the business team. One of the business team members had actually been tracking defect rate to refund rate as a metric, and ended up helping the development team make the business case for focusing on efforts that could help with improving quality.
In the end, with consensus and slightly less skepticism, both teams agreed that extracting the Product query service from the monolith was a good pilot project candidate.
Next steps
In this tutorial you:
- Reviewed the high level planning process
- Reviewed the example monolithic application
- Identified a good candidate for your first microservice
The next tutorial in the collection will discuss how to Scope a microservice extraction from an existing monolith.