development: | |
:url: 'https://api.twilio.com/2010-04-01/Accounts/your_account_sid/Messages.json' | |
:auth_username: username | |
:auth_password: password | |
:phone_from: +1111111111 | |
staging: | |
:url: 'https://api.twilio.com/2010-04-01/Accounts/your_account_sid/Messages.json' | |
:auth_username: username | |
:auth_password: password | |
:phone_from: +1111111111 | |
production: | |
:url: 'https://api.twilio.com/2010-04-01/Accounts/your_account_sid/Messages.json' | |
:auth_username: username | |
:auth_password: password | |
:phone_from: +1111111111 |
Even if no credentials are specified here, we see what parameters should be applied to this adapter. By the way, don’t forget to specify your yml file with the production credentials in gitignore.
Remember to not only establish client connections at an appropriate moment but also to close these connections after their work is done or your app is closed for any reason. In our microservice we used evented tools of EM for this. We’ve built the module App::Initializer
that is responsible for establishing all client connections with external services.
module App | |
module Initializer | |
class << self | |
include EM::Deferrable | |
def load_app | |
#log that app loaded all files and ready to start from this point | |
succeed | |
end | |
end | |
end | |
module Destructor | |
class << self | |
include EM::Deferrable | |
def release_resources | |
#log that app is closing. Think about a way to make this logic be able to differ closing reasons | |
succeed | |
end | |
end | |
end | |
class << self | |
extend Forwardable | |
def root | |
@root ||= File.dirname(File.expand_path('..', __FILE__)) | |
end | |
def environment | |
@default_env ||= ENV['SMSERVICE_ENV'] || 'development' | |
end | |
alias env environment | |
def_delegator Initializer, :load_app, :init | |
def_delegator Destructor, :release_resources, :close | |
end | |
end |
At the time of booting libraries, each adapter binds callbacks with the instantiating client connections to services to the App::Initializer
module. At the same time, each adapter also binds callbacks with the logic of closing these connections to the App::Destructor
module.
All code, where network requests are applied, should be logged in case something goes wrong, especially if your microservice is somehow bound with payment mechanisms. You should not have blind zones.
From the moment you start building microservices, you are totally responsible for how they will behave, so it is better to keep more log calls than you think you need and remove them later than not to have enough data to recover what has happened.
The link to the repository of the demo project can be found here.
We’ve built a small API that will work with an SMS service. The functionality of the SMS service is not huge but it is enough to explain the things. It is also quite universal and can be reused in the future. We will use Redis as a message bus for the communication between the main app and the microservice.
Here is the scenario of our APP workflow:
The scenario of this SMS workflow is as follows:
A microservice is nothing more than a separate app like any Rails application. It also has a gemfile (optionally), library dependencies, and initializers. Frankly speaking, it will be a simple daemon process with an infinite loop based on the evented architecture. There will be EventMachine (EM) responsible for all this. It (EM) deserves a separate article, so we won’t look into it. But you should definitely learn more about it.
As said earlier, the SMS waits for data from Redis and then makes a request to the API service responsible for sending the SMS (in our case it’ll be Twilio).
It’s time to take a closer look at the SMS service. There is no Rails anymore, only plain Ruby.
The server.rb
file will be the entry point of our SMS microservice.
To instantiate the process, we need to open the SMService directory and call ruby server.rb
from there. After that require './config/boot'
is called.
boot.rb
is responsible for requiring all SMS dependencies and initializing configurations:
Loading:
Initializing:
After everything is loaded and initialized, App.init
is called and delegates to invoking all callbacks that establish client connections to the external API (in our case it is just Redis).
Let’s look at all this in action.
Redis should already be started. If you don’t have it yet, you need to install it, launch and then return to continue.
Start the APP by calling rails s
from test_rails_backend/api
.
Start the SMService by calling ruby server.rb
from test_rails_backend/smservice
.
Our workflow starts with the POST
request to “/users”
.
Based on how the script workflow should be built within our main app, we can decompose this work into separate service objects, where the collaborators are the object responsible for creating a user (in our case it’s AR) and the object responsible for publishing the created object into the MessageBus.
At this stage the microservice is working and we have subscribed to Redis channel 'users.messages'
.
As a listener, our microservice gets all information that is produced in this Redis channel, then parses the received data and we initialize the SMS object from this parsed data. To make this happen, we should receive a type, phone of recipient, and username.
What comes next? We pass this SMS object to our SMS Messenger which is just an adapter for making requests to Twilio API asynchronously. We assemble headers and the body for this Twilio request.
We will highlight the to_s method call to the SMS object that leads to using the passed SMS type and specified name.
class SMS | |
attr_reader :phone | |
def self.configure(texts:, **args) | |
const_set('TEXTS', texts) | |
end | |
def initialize(type:, phone:, **attrs) | |
@type = type | |
@phone = phone | |
@attrs = attrs | |
end | |
def to_s | |
TEXTS[@type] % @attrs | |
end | |
end |
:texts: | |
registration: "Hello, %{name}! We are glad you are with us now! Sincerely yours, SMService!" |
Finally, if all goes well, the message will be sent to the Twilio API.
And now, let’s start the script one more time without any debugging to see the real operating speed of the workflow. We call the APP first.
A second later, the message is already sent by Twilio.
So we have the APP + SMS working properly. The SMS is not coupled with the APP and if we need to exclude it from our project it will even be possible to delete it from our test_rails_backend
directory, and nothing will change. The APP will just pass the data to Redis after users are created and that’s all. The data will not be processed by the SMS service, but nothing will be broken.
Now it’s cherry pie time! We have just built a fully autonomous microservice for sending messages. If we have another project with the same task to send messages we can do it simply by integrating the existing SMService into it.
Here is how it should be properly done:
yml config
files in the microservice according to the yml.examples
You don’t have to deal with much of code. And as before, this microservice is independent from the main app. You can always exclude or scale it if needed. Meanwhile, the main app remains pure and lightweight.
That’s what is meant by hexagonality: all your features are easy to plug and easy to unplug without being coupled with a project. The only thing we have to do for that is to save the independence of every microservice in the project and don’t let them directly communicate with each other.
As you can see, both monolithic and microservices structures have their advantages and disadvantages, and choosing the right option depends on your deadlines, development team, and the loads which your app will face after deployment.
Be sure that the monolithic structure is good and enough for your app if any of the following options is about you:
-Idea relevance. You have a fresh idea or a startup that should be launched as soon as possible because at the moment it does not have any competitors and your project will have all chances to become successful.
-Tight deadlines for idea implementation.
-Limited budget.
Let’s review when it is better to opt for microservices over monolith.
Monolithic vs. microservices architecture is an easier question to answer if you know in what cases to choose the latter.
Now the monolith vs. microservices choice should be easier to make.
To sum up the discussion of monolithic vs. microservices architecture stated above, it is worth saying that microservices are more interesting for developers to work with. That said it should be also admitted that microservices are a logical stage of a project’s life not every product can come to, especially a startup.
However, this stage is unavoidable if your application reaches consistent growth and you plan to work more on user retention. The closer you are to the critical point when you decide to apply the microservices approach, the more issues you will receive in the end. You should remember that the process of decomposing your app into microservices is quite time-consuming.
If you have to decide whether it is reasonable to apply the microservices architecture in your startup, keep in mind that there should be more reasons than just guesses that your project will become successful.
There are cases when monoliths are a better option in the question “monolithic vs. microservices architecture”. Microservices development requires more time, thus sometimes it is better to create a monolithic app fast that will later be decomposed into microservices when the need arises in line with the provided DevOps services.
MLSDev team knows how to work with both microservices and monoliths correctly. We are ready to share our experience with you and help you choose the right architecture for your application.
Contact us to learn more.