There are two major types of software architecture: monolithic and microservices. The latter has become extremely popular in recent years. At the same time monoliths are still there and programmers work with them as well. It means that for many the question “monolithic vs. microservices architecture” is still topical when it comes to app development.

In this article we are going to help you decide which architecture is better for your project. We will review microservices and monoliths from the business and technical perspectives, tell you about their pros and cons, and show a demo microservice application that works on Ruby.

What is a Monolithic Architecture?

Let’s begin with the monolithic structure as it is still considered a traditional way of building apps.

Definition of a Monolithic Architecture

Monolithic Architecture Diagram
Monolithic Architecture Diagram

A monolithic architecture is a model of software structure that is created as one piece where all Rails tools (ActionMailer, ActiveJob, ActionCable, etc.) might be gathered together with the code that these tools applies. The tools are not connected with each other but they are also not autonomous.

If changes are required within one feature, it will influence the work of the entire process and other features because they are parts of one process.

Let’s recall what Ruby on Rails is out of the box in general, what it can offer, its advantages and disadvantages.

Rails is a framework that covers almost all the use cases and development needs. What is especially good is that it is easy to work with.

As soon as you write rails new you immediately get a new application. Right after that you can create any REST API you want and use Rails helpers and generators, which makes development even easier.

Do you need to send emails in your Rails app? Use Rails ActionMailer. Or maybe you have to do some hard processing? ActiveJob can help you with that. With Rails 5 you will also be able to use websockets out of the box. It will be easy to create chats or make your application more interactive.

You can use all that and even more immediately provided that you use correct DSL syntax. You don’t have to know everything about the internal implementation of these tools, it is enough for you to know that it works, consider it’s DSL, and receive the expected result.

Monolith development seems to be very convenient but there is much more to know about it. For this, we need to analyze its advantages and disadvantages.

Monolithic vs. Microservices Architecture: Pros of Monolithic Applications

Advantages and Disadvantages of Rails Out Of The Box
Advantages and Disadvantages of Rails Out Of The Box

We will start with positive sides any monolithic application has. The benefits are:

  • Easy to develop because of the big variety of available tools that are ready to be integrated into apps. Simple to deploy because all actions are performed with one directory at once.
  • Fast to develop because all of these Rails tools require minimum effort to be applied. The monolithic architecture means that you don’t need to think how all works in tools like ActiveJob, ActionMailer or ActionCable.

The monolithic architecture has one major advantage - simplicity. Due to its simple structure there is no need to perform many complicated operations and extra activities that are required for complex systems.

Monolithic vs. Microservices Architecture: Cons of Monolithic Applications

Simplicity is good but there are cases when it is not enough to be completely happy with the process of app development and its results. Let’s review the drawbacks of monolithic software:

  • With every new approach integrated into your app, you have more and more dependencies on external libraries. Your codebase becomes large. This makes the workflow more difficult.

    What is more important, all of your machines become more wasteful in terms of power and money. And if your monolithic application becomes unsupported, you don’t have many options except starting a new app from scratch or decomposing current app into microservices.

  • Every Rails tool works following the principle of a black box. It’s the characteristics not only of Rails, but of any external library. It doesn’t bother you until your task is simple and Rails can take care of it.

    If the task is complicated, it will be hard to monkey patch the use of the tool according to your needs and keep it synchronized with its source. Additionally, all these out-of-box tools are made to cover all possible use cases, that you may not need but have no choice to cut out.

  • Monolithic applications are not reusable because they use tools out of the box. You will not be able to just extract a feature and integrate it into another project because it depends on the entire Rails ecosystem. This means that it won’t work outside of your app.

    Although the hexagonal principle is not applied most of the time, it’s more flexible when all of your features are developed based on it and work like plugins (at the end of the article you will definitely understand what is meant by it).

  • Last but not least. Every tool included into a monolithic application becomes a part of its ecosystem. It means that in case of a code failure this invalid segment can negatively influence the stability of work of the entire app.

    Reasons for that can be different. It might be a syntax error in the code, invalid merge commits and even a runtime code failure at the very worst. Generally speaking, one small mistake can destabilize the work of the entire monolithic app and the team that develops it.

Microservices vs. Monolith: Diagram for Monoliths

All information about the monolithic architecture that is mentioned above can be shown in the diagram.

Pros and Cons of the Monolithic Architecture
Pros and Cons of the Monolithic Architecture

Though development from scratch is fast and easy, with every new feature your monolithic application becomes bigger and heavier. Of course, this is reflected in the work of the entire app.

To understand which option wins in the battle monolithic vs. microservices architecture, it is time to analyze the latter.

What is a Microservices Architecture?

Microservices appeared as an alternative to monoliths in order to solve all issues and bottlenecks caused by the limitations of the monolithic architecture.

Microservices Definition

Microdervices Architecture Diagram
[Microdervices Architecture Diagram

So, what are microservices in general? These are standalone processes which serve certain purposes but they have far less responsibilities comparing to common Rails apps. Ideally they have to serve only one purpose according to the SRP.

Let’s consider an example. As you know, our brain consists of two hemispheres. The left one is responsible for rational and logical things while the right hemisphere performs functions that are connected with creativity. They don’t delegate their tasks to each other but they have some communication channels. Moreover they work concurrently and that’s exactly what we want. Our aim is to build architecture where microservices would work concurrently. I suggest that you view Rob Pike talk “Concurrency is not a parallelism” if you haven’t seen it yet.

We will review microservices and designing the microservices architecture in detail further when it comes to their practical use.

It’s time to show the advantages and disadvantages of the microservices approach.

Monolithic vs. Microservices Architecture: Benefits of Microservices

Microservices Advantages and Disadvantages
Microservices Advantages and Disadvantages

In the question of monolithic vs. microservices architecture, the latter has many advantages that can easily outweigh the benefits of monoliths.

The advantages of microservices are as follows:

  • We do not just cover use cases when we apply a microservice approach. By decoupling the microservice from the main app and delegating a task to a separate service we save our application from getting potentially bigger and heavier. As a bonus, instead of one heavy process we will have several processes with a correct server load distribution.
  • If a part of your microservices ecosystem becomes a bottleneck, you are free to scale it horizontally, independently of other app components. All you should do is organize correct communication between these services. If you don’t need the microservice anymore, you can unplug it easily without the risk of breaking something in the main app (depends on your communication type).
  • All microservices require working by the black box principle. Nevertheless, you are always free to change something if needed to make sure there is no overhead.
  • Microservices architectural style promotes reusability. If we take care of microservices independence from the project we will receive autonomous microservices that can be used in other projects for similar tasks.
  • All microservices can be fully autonomous. Thus, any collapse in one of them won’t affect the work of the main app. Of course, this collapse will destabilize microservice it is responsible for, but all the rest will remain stable. Testing becomes easier when microservices are separated. You don’t have to boot the whole system to run unit tests on the microservice.

Despite so many positive sides, microservices have certain drawbacks. It's time to review them in order to understand whether they are more significant than their benefits.

Monolithic vs. Microservices Architecture: Microservices Disadvantages

Having so many advantages doesn’t mean that the microservices architecture is a silver bullet. It has some pitfalls as well.

The disadvantages of microservices are as follows:

  • Harder to develop. There is no microservices architecture out-of-the-box as in Rails. You will have to build a pipeline by yourself: loading of libraries, connections initialization, classes ecosystem, and loading of the microservice. You will have to take care of monitoring, logging, and tracing the microservice and the whole system in general because it is unclear how it will work in the production. Moreover, you will need to provide a system that will ensure its fault-tolerance.
  • Take more time for development. The above mentioned disadvantage naturally results in much bigger deadlines than those that are required for customizing the tools out of the box.

Microservices vs. Monolith: Diagram for Microservices

Let’s look at the diagram that describes the cost/features ratio with regard to microservices.

Comparison of Cost/Features Ratio of Microservices and Rails Out Of The Box
Comparison of Cost/Features Ratio of Microservices and Rails Out Of The Box

Any microservice that undertakes some features from the main application, saves it from unavoidable growth and the chance to become unsupported.

This architectural style requires more investment but at the same time it doesn’t let you pass the point of no return. Even if this somehow happens, you will still have the microservices ecosystem and its wide responsibilities which means that it won’t be necessary to develop everything from scratch.

Furthermore, microservices can save your time in future while dealing with the task that has been accomplished earlier.

Microservices Example: Practical Use

We cannot get down to practice and code at once because the question of building microservices also touches upon the matter of architecture and interaction between the microservice and the main app. These points are more important than the code of your microservice. Don’t forget that it is different from what you get with Rails out of the box.

Now your application consists of the main app, microservice, and communication between them. Chances are that it will work for one or two requests locally but will not work in production, so you should treat it more judiciously than just the code of your microservice and everything that is in between.

If you have no experience in these questions, go on reading my know-hows below. You could use them until you have your own tricks and approaches.

Know-How: Design Microservices Architecture and Communication Between the Main App and the Microservice

1. Prefer to use simplex communication between the main app and the microservice. Otherwise you may have issues with synchronization when some of your components in this pipeline have performance issues or are not available for request.

For example:

  • correct way: Main App → microservice → 3rd Party API
  • wrong way: Main App ⇄ microservice ⇄ 3rd Party API

2. It’s better to build communication between a microservice and the main app using the publish/subscribe pattern, not the observer pattern. Apart from hexagonality, it also can bring concurrency.

This is appropriate if there are more than one microservice in our architecture and they are do not require sequential processing. Moreover, if you use direct communication between the main app and the microservice, the microservice will be available at some port. You will have to foresee its unavailability for the outside world (today is 2108 and we have Kubernetes, Docker, an others that will help us do it but we should still be careful).

If you choose communication via pub/sub (Message Bus), you should invoke your microservice as a daemon process and the issue mentioned above will be solved itself.

Imagine that we have the main app that performs a required function, pushes data to a message bus and all of its listeners get it immediately. Websockets microservice, Mailer microservice, SMS microservice and other assumed microservices receive this data co-instantaneously and process it concurrently. This seems to be a more effective solution than direct communication between them, doesn’t it?

Data Flow in the Microservices Architecture
Data Flow in the Microservices Architecture

In the meantime, our main task is just to build a right architecture of microservices that are not dependent on each other or a sequential processing. It has an explanation in the book of Sam Newman “Building Microservices” in the chapter “Orchestration vs. Choreography” where these two techniques are reviewed.

3. Decompose into a microservice the functionality that does not demand any communication there and back with the main app (e.g. sender of emails, SMS, push notifications or other requests to the third-party API). If you have considered all of the three points, you should have received the architecture that will not let the the entire app crash even if your microservice becomes inactive.

4. Make sure you log all places of control transferring, especially if your application contains payment mechanisms, to be able to recover what has happened. In this case user’s budget imposes additional responsibility on you.

Know-How: Building Microservices

Now you have the main app and you know how it will communicate with your microservice. If they communicate directly with each other you should make the microservice unavailable for the outside world.

If you communicate via the Message Bus, you should invoke your microservice as a daemon process and the issue mentioned above will be solved itself.

Now we can move to the process of building microservices. It has its own rules that we should follow. No matter how perfect the code of your microservice is, you may face issues with support and development if the microservice architecture doesn’t work according to certain
rules.

The following rules can help you with microservices a lot:

1. You have to do everything by yourself because you do not have any Rails and architecture out of the box that can be started by one command. Your microservice should load libraries, establish client connections, and be able to release resources if it stops working for any reason.

It means that being in the microservice folder and having made the ruby server.rb command (a file for starting a microservice) we should make the microservice do the following:

  • Load used gems, vendor libraries (if used), and our own libraries
  • Use the configuration (depend on the environment) for adapters or classes of client connections
  • Establish client connections (permanent connections are meant here). As your microservice should be ready for any shutdowns, you should take care of closing these client connections at such moments. EventMachine and its callback mechanism helps a lot with this.
  • After that your microservice should be loaded and ready for work.

2. Incapsulate your communication with the services into abstractly named adapters. We name these adapters based on their role (PubSub, SMSMessenger, Mailer, etc.). This way, we can always change the inner implementation of these adapters by replacing the service if the names of our classes are service agnostic.

For example, we almost always use Redis in our application from the very beginning, thus it is also possible to use it as a message bus, so that we don’t have to integrate any other services. However, with the application growth we should think about solutions like RabbitMQ which are more appropriate for cases like ours.

As well check whether the abstract names for your adapters are free and are not used in any active gems or vendor libraries.

3. If your code is designed in such a way that your classes are coupled with each other, do it according to the dependency inversion principle. This will help your code to avoid issues with lib booting.

4. Design your adapters to load configs from the environment (according to [12factor](https://12factor.net/config)). If you prefer using yml files to env variables, you can decompose your responsibilities more clearly according to the name of the yml. Take a look at the example of yml for configuring the adapter in order to deliver SMS messages (Twilio in our case):

 

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
view raw app.rb hosted with ❤ by GitHub

 

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.

How Does the Microservices Application Work?

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:

  • POST “/users” when a user signs up.
  • Create User: save to database, publish user to Redis
  • Render the data.

The scenario of this SMS workflow is as follows:

  • Receive and parse the data from Redis.
  • Send the structured data to the external API that is responsible for sending an SMS.

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:

  • Gems, some standard libraries (yaml, etc.)
  • Our custom libraries for handling the SMService purpose.

Initializing:

  • Connection to the message bus (in our case it is Redis). EM features help us to provide it in an evented way by adding callbacks to the Constructor, which will invoke these callbacks when the microservice is ready to start. We have also added a callback to the Destructor to close this connection later when the microservice is closed.
  • Templates for the SMS texts.
  • Client configurations to communicate with Twilio. We do not use any official gem for this purpose because in this case we lose the asynchronism provided by the EM. We have to build our own adapter for Twilio API.

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).

Microservices Application Workflow

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”.

 

POST Request to “/users”
POST Request to “/users”

 

Create User 1
Create User 1

 

Create User 2
Create User 2

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.

 

Conect
Conect

 

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.

 

Deliver
Deliver

 

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
view raw sms.rb hosted with ❤ by GitHub

 

:texts:
registration: "Hello, %{name}! We are glad you are with us now! Sincerely yours, SMService!"
view raw sms.yml hosted with ❤ by GitHub

 

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.

 

Request
Request

 

A second later, the message is already sent by Twilio.

 

Send a Message via Twilio
Send a Message via 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.

Resuming the Process of Building Microservices

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:

  • Clone SMS to your project
  • Build your own mechanism of pushing users to Redis users.messages channel in the main app
  • Register at Twilio or, if you already have an account, just specify its credentials in certain files
  • Fill all yml config files in the microservice according to the yml.examples
  • Profit!

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.

 

Microservices vs. Monolith: Choosing the Best Fit

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.

Conditions for Using Monoliths

Be sure that the monolithic structure is good and enough for your app if any of the following options is about you:

  • Small app. Your app is quite simple and does not meet serious loads after deployment or it is already in production but does not face any issues with performance yet.
  • Quick launch
  • -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.

    • Proof of concept. You want to check whether your idea is feasible and useful by building a basic product for your target audience.
    • No experience with microservices. Your team is not big enough to separate people for designing separate microservices or your team has no experience in building them. Of course, it is better to start learning and practising microservices but the features that should be the skeleton of your app are not the best opportunity to do it.

    Let’s review when it is better to opt for microservices over monolith.

    Criteria for Choosing Microservices

    Monolithic vs. microservices architecture is an easier question to answer if you know in what cases to choose the latter.

    • Complex app. Your app is complex enough for integrating new tools or it experiences issues with the load that cannot be solved by vertical scaling or it is unprofitable in this case.
    • Plan to grow and scale the app. Your project experiences a stable load growth and you plan to integrate new tools, but there are chances that the app will reach a critical point when scaling issues may appear.
    • Experience with microservices. There are developers in your team who are experienced in designing and deploying microservices to production and you are sure that there will be a part of the team who will support and develop the main app while they are working on microservices.
    • Utopia. You have enough money, loyal managers, and extended deadlines.

    Now the monolith vs. microservices choice should be easier to make.

    Monolithic vs. Microservices Architecture: Business Perspective

    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.

    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.

    Get in touch