I really hate the saying "best practices" mainly because it creates a belief that there is only one right way to do things. But I wanted to put together a post around some ideas for strengthening your micro services architectures.
As I’ve previously discussed, Micro service architectures are more complicated to implement, but have a lot of huge benefits to your solution. And some of those benefits are:
- Independently deployable pieces, no more large scale deployments
- More focused testing efforts
- Using the right technology for each piece of your solution
- I creased resiliency from cluster based deployments
But for a lot of people, including myself, the hardest part of this process is how do you structure a micro-service? How small should each piece be? How do they work together?
So here are some practices I’ve found helpful if you are starting to leverage this in your solutions.
One Service = One Job
One of the first questions is how small should my containers be. Is there such a thing as too small? A good rule of thumb to focus on is the idea of separation concerns. If you take every use-case and start to break it down to a single purpose, you’ll find you get to a good micro-service design pretty quickly.
Let’s look at examples. I recently worked on a solution with a colleague of mine that ended up pulling from an API, and then extracting that information to put it into a data model.
In the monolith way of thinking, that would have been 1 API call. Pass in the data and then cycle through and process it. But the problem was throughput, if I would have pulled the 67 different regions, and the 300+ records per region and processed this as a batch, it would have been a mess of one gigantic API call.
So instead, we had one function that cycled through the regions, and pulled them all to json files in blob storage, and then queued a message.
Then we had another function that when a message is queued, will take that message, read in the records for that region, and process saving them to the database. This separate function is another micro-services.
Now there are several benefits to this approach, but chief among them, the second function can scale independent of the first, and I can respond to queued messages as they come in, using asynchronous processing.
Three Words… Domain Driven Design
For a great definition of Domain-Driven Design, see here. The idea is pretty straight forward, the idea of building software and the structure of your application should mirror the business logic that is being implemented.
So for example, your micro-services should mirror what they are trying to do. Like let’s take the most straightforward example… e-commerce.
If we have to track orders, and have a process once an order is submitted of the following:
- Orders are submitted.
- Inventory is verified.
- Order Payment is processed.
- Notification is sent to supplier for processing.
- Confirmation is sent to the customer.
- Order is fulfilled and shipped.
Looking at the above, one way to implement this would be to do the following:
OrderService: Manage the orders from start to finish.
OrderRecorderService: Record order in tracking system, so you can track the order throughout the process.
OrderInventoryService: Takes the contents of the order and checks it against inventory.
OrderPaymentService: Processes the payment of the order.
OrderSupplierNotificationService: Interact with a 3rd party API to submit the order to the supplier.
OrderConfirmationService: Send an email confirming the order is received and being processed.
OrderStatusService: Continues to check the 3rd party API for the status of the order.
If you notice above, outside of an orchestration, they match exactly what the steps were according to the business. This provides a streamlined approach that makes it easy to make changes, and easy to understand for new team members. More than likely communication between services is done via queues.
For example, let’s say the company above wants to expand to accept Venmo as a payment method. Really that means you have to update the
OrderPaymentServices to be able to accept the option, and process the payment. Additionally,
OrderPaymentService might itself be an orchestration service between different micro-services, one per payment method.
Make Them Independently Deployable
This is the big one, if you really want to see benefit of microservices, they MUST be independently deployable. This means that if we look at the above example, I can deploy each of these separate services and make changes to one without having to do a full application deployment.
Take the scenario above, if I wanted to add a new payment method, I should be able to update the
OrderPaymentService, check-in those changes, and then deploy that to dev, through production without having to deploy the entire application.
Now, the first time I heard that, I thought that was the most ridiculous thing I ever heard, but there are some things you can do to make this possible.
- Each Service should have its own data store: If you make sure each service has its own data store, that makes it much easier to manage version changes. Even if you are going to leverage something like SQL Server, then make sure that the tables leveraged by each micro-service are used by that service, and that service only. This can be accomplished using schemas.
- Put layers of abstraction between service communication: For example, a great common example is queuing or eventing. If you have a message being passed through, then as long as the message leaving doesn’t change, then there is no need to update the receiver.
- If you are going to do direct API communication, use versioning. If you do have to have APIs connecting these services, leverage versioning to allow for micro-services to be deployed and change without breaking other parts of the application.
Build With Resiliency in Mind
If you adopt this approach to micro-services, then one of the biggest things you will notice quickly is that each micro-service becomes its own black-box. And as such, I find it's good to build each of these components with resiliency in mind. Things like leveraging Polly for retry, or circuit breaker patterns. These are great ways of making sure that your services will remain resilient, and it will have a cumulative affect on your application.
For example, take our
OrderPaymentService above, we know that Queue messages should be coming in, with the order and payment details. We can take a microscope to this service and say, how could it fail, it's not hard to get to a list like this:
- Message comes through in a bad format.
- Payment service can’t be reached.
- Payment is declined (for any one of a variety of reasons).
- Service fails while waiting on payment to be processed.
Now for some of the above, it's just some simple error handling, like checking the format of the message for example. We can also build logic to check if the payment service is available, and do an exponential retry until it's available.
We might also consider implementing a circuit breaker, that says if we can’t process payments after so many tries, the service switches to an unhealthy state and causes a notification workflow.
And in the final scenario, we could implement a state store that indicates the state of the payment being processed should a service fail and need to be picked up by another.
Consider Monitoring Early
This is the one that everyone forgets, but it dove-tails nicely out of the previous one. It’s important that there be a mechanism for tracking and monitoring the state of your micro-service. I find too often it's easy to say “Oh the service is running, so that means it's fine.” That’s like saying just cause the homepage loads, a full web application is working.
You should build into your micro-services the ability to track their health and enable a way of doing so for operations tools. Let’s face it, at the end of the day, all code will eventually be deployed, and all deployed code must be monitored.
So for example, looking at the above. If I build a circuit breaker pattern into
OrderPaymentService, and every failure updates status stored within memory of the service that says it's unhealthy. I can then expose an http endpoint that returns the status of that breaker.
- Closed: Service is running fine and healthy.
- Half-Open: Service is experiencing some errors but still processing.
- Open: Service is taken offline for being unhealthy.
I can then build out logic that when it gets to Half-open, and even open specific events will occur.
Start Small, Don’t Boil the Ocean
This one seems kind of ironic given the above. But if you are working on an existing application, you will never be able to convince management to allow you to junk it and start over. So what I have done in the past, is to take an application, and when you find its time to make a change to that part of the application, take the opportunity to re-architect and make it more resilient. Deconstruct the pieces and implement a micro-service response to resolving the problem.
Stateless Over Stateful
Honestly, this is just good practice to get used to, most container technologies, like Docker or Kubernetes or other options really favor the idea of elastic scale and the ability to start or kill a process at any time. This becomes a lot harder if you have to manage state within a container. If you must manage state, I would definitely recommend using an external store for information.
Now I know not every one of these might fit your situation but I’ve found that these ten items make it much easier to transition to creating micro services for your solutions and seeing the benefits of doing so.