Any software engineer has heard of the term “microservices.” The main reason for this is that this architecture allows you to divide the application into small self-contained applications, with significant advantages such as faster and lower-cost scaling, a smaller and more readable codebase, and faster development and release of features if properly planned.
Shared libraries are an essential component of microservice architecture. In this article, I will go over the dos and don’ts of implementing them in a microservice architecture. So let’s get started.
What is Shared Libraries and How Do They Work?
Just a refresher for people who don’t remember but essentially, Microservices architecture allows components to be deployed independently. Unfortunately, only a few teams have achieved it in my consulting and software development experience. Deployment independence is essential because it allows for true agility and reduces the amount of communication between different teams. On the other hand, microservices are tightly coupled, which in my experience, introduces a lot of complex dependencies due to shared libraries. But before we move on, what are shared libraries?
In a nutshell, if two microservices are expected to share code, the code can be placed in a shared library. This essentially means that the code is extracted from the microservice and packaged so that other microservices can use it. However, an implementation like this requires that the microservices be written to allow for the use of a shared library. For instance, when they are written in the same language or, at the very least, use the same platform, such as JVM or.NET CLR.
Moreover, this means the microservices become interdependent because of the shared library, and the library work must be coordinated. Both microservices’ features must be implemented in the library, and each microservice is impacted by changes made to the other microservice via the backdoor. Many developers know that this can lead to many mistakes, which may require the teams to coordinate the library’s development. In addition, changes to a library may necessitate the deployment of a new microservice in certain circumstances. For example, a security hole in the library has been filled.
If there are so many problems associated with Shared Libraries, why do we use them? The most straightforward answer that I can give is that the benefits outweigh the problems in specific scenarios. However, I will say that there may be some instances when shared libraries shouldn’t be used. However, shared libraries become a necessity when creating large complex systems whose benefits cannot be ignored. Let’s now look at some Dos and Don’ts of Shared Libraries.
What are the Dos and Don’ts of Shared Libraries and How Do They Work?
As the creator of Python, Guido Van Rossum, once said, “Code is read more often than it is written.” Thus, it becomes a necessary task to avoid code duplication as much as possible. Using shared libraries between microservices is one solution. Logging is one of the most common applications for shared libraries. Custom logic can be added to logging, such as formatting or hiding sensitive information like customer addresses and phone numbers.
Collecting, standardizing, and consolidating log data from an IT environment to facilitate streamlined log analysis is called log aggregation. Developers would have to manually organize, prepare, and search through log data from multiple sources without log aggregation in order to extract useful information.
Now in my experience, to ensure that software engineers get the most from their logs, there are a few things software engineers need to do to ensure they’re helpful and any service can understand them. In addition, putting in some effort upfront on each of your services and possibly even creating a set of tools to use between them can be an excellent first step toward ensuring your logs are valuable and insightful.
Therefore, logging is an excellent example of a shared library because it appears in every microservice you create (hopefully) and isn’t dependent on any of them. Likewise, security, monitoring, async communication, and exceptions are great examples of shared libraries. Some other advantages include:
- They use less disk space. It occurs as the shared libraries are not included in the executable programs.
- It uses less memory as the shared library code is only loaded once.
- Sometimes when working with large complex files, shared libraries will reduce load times. This is because the shared library code may be loaded in memory.
- It may improve performance because fewer page faults are generated. After all, the shared library code may already be loaded in memory.
Now let’s go over some guidelines to remember when using shared libraries in a microservice architecture:
Don׳t Create a Single Library
You can manage your shared libraries in various ways. However, I recommend creating either a separate repository for each required library or a single repository (also known as a monorepo) with multiple libraries. The important thing is that they are separated in some way. For example, introduce a single repository that includes monitoring, security, and logging projects. These will all be self-contained (except if there are needed dependencies).
Do Encapsulation
It’s critical to encapsulate the user’s internal decisions and logic; it’s essential to programming and a shared library. In addition, encapsulation is necessary for unwanted access, preventing data breaches. So it is a necessity when implementing shared Libraries.
Imagine you’re creating a library with vendor-specific code, such as a library to upload images to storage. Then interfaces/classes that the client uses must be designed generically, and names like “S3ImageUploader” should be avoided (s3 is an Amazon service for file storage). But why should you avoid such naming schemes? For example, let’s say you decide to switch to Azure Blob (the Microsoft equivalent of Amazon S3) in the future; all of your clients will need to update their method signatures. Therefore, interfaces exposed to the user should be given abstract names like “ImageUploader.” Trust me, it will save you a lot of time and effort in the long run.
Mitigate the Damage
Sometimes you will have to break the code, such as when you need to replace an internal library due to a newly discovered vulnerability that affects the output.
In such cases, use semantic versioning to create versions that follow the MAJOR.MINOR.PATCH pattern, allowing your clients to upgrade to the latest version. Changes in the major version inform users that this version may break compatibility, making them decide whether or not to upgrade. Again, conventional commits come to the rescue! Semantic versioning can be a pain to manage manually (I’ll get to that later).
Now, another alternative would be to create a new interface with the new logic built in rather than altering the existing interface behavior. This way, you can use the existing code, but you must deprecate the old interface and only support the new one with additional functionality. Based on my experience, customers will be more likely to adopt the new interface (with the new behavior).
Keep it Clean!
Although many people may contribute to the shared library, don’t turn it into a monolith! Before introducing a new library, give it a lot of thought, and when you do, consider how it might change and who might use it. Please don’t make it for your own needs only, making it difficult to use or extend for others.
No domain-specific code is allowed! Even shared business code is most likely not supposed to be there! Even if it means all microservice that utilize it must copy it, a user model that starts the same for all microservices is still domain-related logic that doesn’t need to be in the library. This is because different microservices may need to modify it in the future to meet their specific business requirements. It’s absurd that they all use the same model because it can introduce fields or logic that aren’t related to other MS or even break them if they want to rename or change some of the logic.
Therefore, conventional commits are a great way to communicate code changes when working with a single repository. So use commit conventions, such as beginning each commit with a fix. This comes in handy when someone else tries to understand the repository’s history, what was done, and wherein the code.
How to Make Your Own Gem in Ruby?
Gem in Ruby is a software package manager which allows Ruby applications and libraries to be distributed in a single format. Ruby gems are the gold mine in Ruby programming. They allow developers to add additional functionality to your code alleviating the need to write the same code repeatedly.
Gems are prevalent in Ruby. If you have ever used rspec tests, bundler and rack in Ruby, then you have used gems, as these are all gems!
Now let us look at how you can make your gem in Ruby.
Step 1: Naming And Building Your Gem
When making a gem, the first thing we’ll want to do is come up with a name and a concept. After that, we’ll want to build out your basic gem. For this article, I’ll be making a straightforward gem to wish someone a happy birthday. Check if the name you choose for your gem is available on Ruby before using it. This is because RubyGems does not permit duplication of gems. Once you’ve verified it, you can move forward with creating your gem.
It’s obvious from looking at RubyGems that we didn’t decide to make our gem on our own. Someone even developed a “bundler” gem that was made especially to support our gem. Run the following command to set up the bundler gem:
gem install bundler
Then run the command below to create a directory for us to work in:
bundle gem your_gem_name
We will create a happy_birthday gem directory. Let’s now move on to the next step.
Step 2: Gemspec and Version
When you open the happybday.gemspec file created by bundler, it will look something like this:
Lib = File.expand_path("../lib", __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
Require "happy_bday/version"
Gem::Specification.new do |spec|
spec.name = "happy_bday"
spec.version = HappyBday::VERSION
spec.authors = ["John_Doe"]
spec.email = [""]
spec.summary = %q{Used to wish a user happy birthday on their birthday.}
spec.homepage = "https://girhub.com/John_Doe/happy_bday"
spec.license = "MIT"
# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
'Git ls-files -z'.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
end
spec.bindir = "exe"
spec.executables = spec.files.gre[(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
spec.add_development_dependency "bundler", "~> 2.0"
spec.add_development_dependency "rake", "~> 10.0"
spec.add_development_dependency "rspec", "~> 3.0"
end
Fill in the authors, email, summary, and homepage fields to update the top part.
There’s a file named version.rb in the lib/happy_bday directory that you can find. With bundler, this is automatically produced, and its contents will look something like:
Module HappyBday
Version = "0.1.0"
end
We may continue with the current version. We’ll update this page if we decide to later enhance or add more features to our gem.
Step 3: Coding Your Gem
Here is the main part. We will now write code to make our gem work. First, open the lib/happy_bday.rb file. All the code that needs to be added goes into this file. Here is what bundler has automatically generated upon creating the file. It also includes directions on where to input our new code:
Require "happy_bday/version"
module HappyBday
Class Error < StandardError; end
# Your code goes here...
end
Now let us write some basic code. For this example, we will create a class method to wish the user a happy birthday when called. Here is the code that makes the magic happen:
Require "happy_bday/version"
module HappyBday
Class Error < StandardError; end
def self.happy_bday
puts "happy birthday"
end
end
We have now completed our basic class. Now is the time to test it out!
Step 4: Testing Your Gem
Ruby has many great ways to test out your gem, each having its strong suits, including using rspec tests or irb. For this example, we will be using irb, but rspec is another great resource for testing your code.
Before we move on to testing our gem, we must first build it. To build your gem, run the following command in your terminal to create your gem:
gem build happy_bday.gemspec
Once you receive a success message confirming your build, we can move on to installing our gem. Run the gem install ./your_gem-version.number.gem to install your gem. For this example, we run the following command:
gem install /.happy_bday-0.1.1.gem
Once our gem is installed, you will get a confirmation message in your terminal.
This means we are ready to test our gems’ functionality. Open up your terminal and drop into irb. Next, run the require ‘happy_bday’ command to make sure our irb session can use our new gem. Now, let us test if our code works. Call the happy_bday method and check if it works. Your code should look something like this:
// irb
[2.6.1 :001 > require 'happy_bday'
=> true
[2.6.1 :002 > HappyBday.happy_bday
Happy birthday
=> nil
As you can see, irb printed out ‘happy birthday,’ which matches the code we wrote. This means our gem is up and running, and now it’s time to push our gem to the world.
Step 5: Pushing Your Gem Live
To make your gem public; first, you’ll need to create an account on RubyGems. After you have created your account, all you need to do is push it live. You’ll be asked to enter your account credential, which will be created upon successful login.
// gem push happy_bday-0.1.1.gem
Enter your RubyGem.org credentials.
Don't have an account uet? Create one at https://rubygems.org/sign_up
Email:
Password:
Signed in.
Pushin gem to https://rubygems.org...
Successfully registered gem: happy_bday (0.1.1)
This will now publish your gem on the Ruby website. You can now head over to rubygems.com and check out your new gem.
Wrapping Up
Congratulations! You have now learned everything there is to know about shared libraries and how to make and publish your own gem. After reading this article, you should aim to create shared libraries that save your firm time and money by minimizing code duplication and resolving issues caused by poorly written shared libraries. Then, of course, you can experiment with various approaches later, but only after you understand why and how shared libraries are designed and implemented the way they are now.
Interested in joining our Technology team at Dialectica? Visit our dedicate Technology Careers page to explore our Software Engineer, Cloud Engineer and more tech job vacancies. Apply today for our Senior Data Engineer, Business Intelligence (BI), Full Stack, Front-end Developer, Back-end Developer jobs in Athens and become a member of #teamDialectica.