In 2012, I was hired to lead a team of developers at the faculty of medicine at a Canadian university. The portfolio consisted of over 40 applications, some very small, that were built in a monolithic fashion in VB.NET using a SQL Server back-end. A few applications were build in other languages, but all ran on Microsoft servers.
The previous team shared functionality between these applications by cutting and pasting code, or by rewriting code to do the same thing over and over again. These applications existed in silos, and important data was often duplicated across applications. Bad!
My challenges, amongst others, were to modernize the team, the technology stack, and the architecture. A few applications had to be decommissioned, and all of the them had to be broken out of their silos and integrated as much as possible. How? Micro Services (& PM2) to the rescue!
(To understand micro services in more depth, follow the link at the bottom of this article, but, my aim is to explain micro services with an easy to understand example.)
Services Solve Common Problems
Here are some simple problems that we had to solve. Follow along, and the concept of "micro services" will become clearer to you.
Sending an email message from a web application is an easy enough thing to do, but what do you do when the network fails, or when the email server is not available? Web applications quit after a predetermined amount of time, or they simply fail. Then, they forget the request forever. Web applications cannot remember to try to resend an email, or to reattempt a task; in fact, they are incapable of initiating any autonomous action whatsoever. Web applications just respond to requests.
Here is an example of something that would go wrong when we sent emails from our web applications at the faculty of medicine: students often filled out forms, but our applications sometimes failed to send followup emails to the students. These messages often included instructions to provide additional information and documents, or to take some other action. Even if we managed these errors, all we could do was notify the user that a message failed. My team would then be notified, but there was no way to easily recover the message.
A standard solution would be to put emails in a message queue to assure delivery - these messages can stay in the queue until they can be sent successfully, and the programmer can trust that messages will be sent without having to consider all of the possible errors. Microsoft provides the means to do this. But not all of our applications were written in the same computer language, using the same technology.
Nor did we necessarily want to tie ourselves to Windows and the .Net framework. We wanted to open up to the possibility of using many of the open source libraries and frameworks that exist out there. We did not want to stay trapped in a Microsoft world.
At first, we looked at RabbitMQ, but our requirements evolved beyond mere messaging queueing. Our users wanted to be able to schedule and store email reminders and send them months into the future. Then, they expressed that they wanted to conditionally send a message - for example, to remind a program administrator to perform a task, but only if the task has not already be performed by someone else.
After a while, the team started to see many other problems that our applications shared. We wanted to solve these problems once, and to make the solutions available to any and all of our applications regardless of operating system, regardless of platform, and regardless of programming language. And, we did not want to impose Unnecessary dependencies on the users of our solution.
We also wanted to publish a simple API for consultants to leverage when they worked with us. We did not want them to spend a long time reading our code.
So? What's the answer? The answer to many of our problems were micro services for many things - but, let's look at a simple micro service to handle our email sending and reminder requirements. We needed the following:
- The means to assure delivery even when emails fail to send.
- The means to schedule emails so that they can be sent some time in the future.
- The means to conditionally send emails.
- The means to deploy this service such that it could be available to applications running on any operating system, on any platform and written in any language.
- The means to simplify and ease development by exposing powerful but easy to use APIs, and by creating as few dependencies as possible.
- The means to securely expose our service so that it cannot be exploited by outsiders.
- The means to ensure that the service starts when the server starts, and stays up no matter what.
A MicroService Is Born!
We used Python because my team had some Python experience, but I introduced Node.js by using PM2 to start and monitor our service.
To solve the problems I have described, and meet the requirements described above, this is what we did. We did not over engineer, or gold-plate our requirements. Keep it Simple, Stupid (KISS) was our watch-word. You Ain't Gonna Need It (YAGNI) was our motto.
- We used an open source database to store our messages. (Rethinkdb, but any DB would do.)
- We wrote a REST style API for client applications to send and schedule messages. (We used Python, but NodeJS would do.)
- Our message transport was HTTPS, and our messages were sent as JSON.
- We wrote a worker to read the database every minute and send messages.
- Each message contained an optional pointer back to the client application - these webhooks are called to ensure that the message is still necessary. If not, the message is simply dropped.
- Each client application was registered with the message service, and secured with a security token.
- We used PM2 to launch and monitor our service and our worker.
Our solution is easy to use - all new applications use it, no matter what language we use. It does not matter that our service in running on Ubuntu; nor does it matter that some of the clients run under Windows. HTTP, REST and JSON are the lingua franca of the Internet!
We even achieved a benefit that we had not expected. We offloaded system latency because our forms no longer have to wait until the email has been sent to return - messages are sent in another process, on another server.
We use PM2 to keep our script alive forever and to make it easier to diagnoze them. We also used PM2 to ensure that our service and the worker restarted when the server restarted. It was seamless and beautiful.
Lastly, PM2 logged errors for us, and it allowed us to monitor the resources our solution was using. When the worker runs, we can see in real time that we use 2% of the available RAM.
What Does It Look Like?
What Other Services Can We Build?
Now that we get the concept, what other services can we build?
A student/resident registration service: At the faculty of medicine we register students, residents and others in the student information system (SIS) at our main campus. Each application that does this needs a special DB2 driver, and a lot of custom code to implement special rules that can change over time. Now that we understand microservices, we will be building a simple service to do this - then we will only need to maintain those rules in one place. This will dramatically reduce the complexity of writing to the SIS.
A workflow engine: Many of our applications manage workflow. We use a workflow service to manage approval processes, or any business process that involves managing transitions from one state to another. Our one workflow engine allows us to author multiple business processes, including transitions and events, and to expose them to multiple applications.
The sky is the limit. What services can you build?
PM2 can help you manage them, monitor them and keep them running. In future articles, we'll show you how.
Resources - Extra reading
72 Resources On Microservices - go nuts and read them all!