Microservice Architectures and Design Patterns

by Julian Duru|

In today's rapidly evolving software development landscape, staying competitive and keeping up with user demands is more critical than ever. Microservices offer a flexible, scalable, and resilient solution to building complex applications. But before we dive into the world of Microservices, let's first take a closer look at Monolithic Architecture.

The Monolithic Architecture is a straightforward and efficient approach to managing application code. By consolidating all code into a single executable program, it simplifies the building, testing, and deployment processes. However, as the application grows in size and complexity, this simplicity can become a disadvantage. With a Monolithic Architecture, implementing any change or update to the system necessitates redeploying the entire application, which can result in downtime and disruption to the entire system. Although, to mitigate downtime, various approaches, such as Canary deployments, can be utilized.

In Microservices Architecture, which is the framework we use heavily at Moniepoint, our approach is somewhat different. The application is broken down into smaller, loosely-coupled services that communicate with each other over well-defined APIs. Each service can be developed, tested, and deployed independently, providing greater agility. With Microservices, developers can focus on building and maintaining smaller, more manageable components, reducing the complexity of the system as a whole.

Benefits of Monolithic architecture over Microservices

  • Simplicity
  • Easier to maintain
  • Easier to deploy

Benefits of Microservice architecture over Monolith

  • Ability to scale different parts of the system independently.
  • Ability to deploy different parts of the system independently.
  • Higher Resilience against failures, since issues are more isolated. 
Screenshot 2023-03-21 at 11.10.39 PM.png

While Microservices are often considered the ideal choice for developing scalable and resilient systems, the tried and tested Monolith cannot be overlooked. However, in many cases, a well-structured Monolith can offer the same level of code cleanliness and maintainability as Microservices, without the added complexity. This well-structured Monolith is often referred to as a Modular Monolith, or Modulith. With a Modular Monolith, you can enjoy the benefits of a Monolith and the flexibility of a modular architecture while maintaining code quality and maintainability. Each module is designed to be isolated from the other modules, resulting in decoupled modules that are easier to maintain.

So, whether you're considering using a Monolith or Microservices for your application, there's no one-size-fits-all solution. It all comes down to your specific needs, requirements, and goals.

A Closer Look at Microservices.

Think about Microservices as a system comprising multiple services where each service is self-contained, and services communicate with each other via APIs or some well-defined mechanism. API, short for Application Program Interface is a medium over which you can invoke functionality within a service. 

Suppose we are building a Hospital Management System (HMS) that provides an API for saving health records. Each health record contains patient information, attending physician, diagnosis, and other pertinent details. The API accepts information from a caller and stores it in a database, with the intention of leveraging it later. For instance, we may provide other APIs to enable users to view the accumulated records, extract valuable insights from the stored data, and perform analyses on the existing records.

Central to the Microservice paradigm is the art of Decomposition. Which is almost like deconstructing a large puzzle. But instead of trying to piece the puzzle together, you're creating a set of smaller, more manageable puzzles. This is a very crucial step while building Microservices.

Decomposition in technical terms stems from decomposition in business terms. Decomposition of the business domain. Your business domain is the totality of all context that pertains to your business. What you want is to properly decompose your business domain into its sub-domains. This means careful organization of functionality within your architecture into individual services, with each service designed to address a specific concern within the business domain (or by extension, the technical domain).

For example, in our Hospital Management System scenario, if I need to send emails to my users, I could have an email service. If I need to send sms to my users, I could have an sms service. If I need to keep track of orders placed by users, I could have an order service. You get the idea.

Note: A monolith will probably suffice for our Hospital Management System, but for the sake of our understanding, let’s assume we are engineering a Microservice setup.

So how can we decompose a system into microservices?

We could be working on breaking apart a monolith into individual services, or building a microservice setup from scratch. Either way, it's great to have a formal approach to decomposition. Personally, I would recommend the following steps:

Isolate and group functionality

First identify the different functionalities that will need to be supported by your system. At a high-level, you can start to see these functionalities if your business domain has been well defined. Functionalities that are related in some way should be grouped together. In grouping these functionalities, you may realize that some of these groups can be handled by 3rd party libraries or existing solutions. In which case, no need to build in-house since existing solutions can be leveraged.

Using the example of our HMS, some functionalities we can discover:

  • As a Patient, I should be able to sign-up on HMS

  • As a Patient, I should be able to log into HMS

  • As a Patient, I should be able to schedule an appointment with a Doctor

  • As a Patient, I should be able to view my scheduled appointments

  • As a Patient, I should be able to view my Health History

  • As a Patient, I should be able to place order for medication using a Doctor's prescription

  • As a Doctor, I should be able to log into HMS

  • As a Doctor, I should be able to schedule appointment with a Patient

  • As a Doctor, I should be able to view my scheduled appointments

  • As a Doctor, I should be able to view a Patient's Health History

  • As a Doctor, I should be able to add a new record to a Patient's Health History

  • As an Admin, I should be able to log into HMS

  • As an Admin, I should be able to onboard a new Doctor on the platform

  • As an Admin, I should be able to view all doctors and patients on the system

  • As an Admin, I should be able to view details of a doctor or patient on the system

This is by no means an exhaustive list, but from the items listed above, you can imagine our system will need to serve different entities: Patients, Doctors, and Admins. An entity is anything of interest in our domain which has an identity and some well known attributes. We can identify 3 functionality groups: one for each identified entity. This could mean assigning a service for each group: patient-service, doctor-service, admin-service. A somewhat simplistic approach, but it might suffice for our use-case. 

Untitled Diagram.drawio (2).png

This arrangement yields some benefit, even in terms of security. Say we want to implement some network level security for the different services, our patient-service can be public facing and available to anybody over the internet; our doctor-service can be deployed behind a VPN (Virtual Private Network) and made only available to users (doctors) within our private network, our admin service can employ even tighter restriction and be made available to IPs in a CIDR range.

Functionalities could also be grouped by other means, including business function. For example, in the list drawn up above, some functionalities revolve around appointments; doctors and patients should be able to schedule and view appointments. We could have an appointments-service which allows users schedule and manage their appointments.

Microservices Architectures and Patterns-Page-12.drawio.png

Using this approach might imply we’ll need to create a service to address each business function within our domain. 

Identify Subdomain Boundaries.

A sub-domain is a logical boundary that separates the different parts of the system according to the business functions and responsibilities. The boundary comprises the classes, entities, components that will exist within each service, including the schema and data that will constitute the database for that service. Establishing boundaries also provides a footing on which to deliberate on the APIs that will be exposed by each service.

What kind of Database? Shared Database or Database per Microservice?

Will a Relational Database suffice? Are we using a SQL or NoSQL database? Is our data inherently graphical and best handled with a Graph database? Do we want a Shared Database or Database for each Microservice? We ask ourselves these questions in order to think about data persistence within our architecture. 

In a Shared Database pattern, you have multiple services connected to a single datasource. All reads and writes by the different services happen against the same database.

Untitled Diagram.drawio (4).png

This has some benefits but also some drawbacks.


  • Simpler to operate.
  • Easier to enforce data consistency.


  • May open up the services to unnecessary coupling.
  • Greater probability of service interference at the database level due to row-level or table-level locks.
  • Scaling becomes trickier. Scaling a service could impact your database and by extension other services.
  • Greater security risk as all services have access to the same data.

In a Database per Service pattern, you have each service connected to its own datasource. This creates greater decoupling of services, since each service handles its own data independently without interfering in other service's data. Services only communicate via APIs. 

Database per Service pattern has several benefits over the Shared Database.


  • Better Scalability
  • Fault Tolerance and Isolation
  • Flexibility
  • Services are more independently deployable.


  • Data Consistency becomes more challenging
  • More expensive to run and maintain
Untitled Diagram-Page-3.drawio (1).png

A hybrid approach, Database per Service Group can equally suffice. Where you can have a group of related services sharing the same database. This approach can allow you to reap the benefits of shared database and database per service. Particularly in cases where it makes sense for the services to share the same datasource.

Untitled Diagram-Page-3.drawio (2).png

Identify Cross-cutting concerns

Cross-cutting concerns affect multiple modules or layers within a system's architecture. Examples of cross-cutting concerns include: security, logging, transaction-management, caching. Security is probably the first concern we might need to address. Our users (regardless of their role) need to be able to log into the platform. 

When a user logs in; they should only be able to carry out those actions they are allowed to do. A patient should not be able to onboard a new Doctor; because that action is restricted to Admins. This security concern cuts across our entire system. For every action that can be invoked by a user, we want to ensure that the user is authorized to carry out such action. We can delegate Authentication and Authorisation to an Auth server, which will exist as a stand-alone service within our infrastructure.

Answer the question: library or service?

A service implies a separate process that is independently deployable (possibly) with its own data storage and exposed API(s). A library on the other hand implies a reusable collection of code that can be imported into other services. A library is not a stand-alone service.  When you group functionality, or identify cross-cutting concerns, it is important to answer the question: should this function group be handled by a library or a service? 

Logging as a cross cutting concern can be conveniently handled by a library; Caching can be implemented by a library, or if we're using a tool like Redis as our cache, that would mean accounting for a redis instance within our infrastructure. Trade-offs will need to be thought through and discussed by the engineering team. 

Identify medium for inter-service communication

How will our services communicate: Soap, REST, GraphQL, gRPC, WebSockets etc? How do we plan to support Synchronous and Asynchronous IO within our infrastructure? Are we using Messaging tools like RabbitMQ, Kafka for asynchronous messaging? How do we plan to support '[Fire and forget]' or '[Guaranteed Delivery]' patterns for our operations. Are we installing an API Gateway? Are we employing [Service Discovery] or do we want services to explicitly know about each other?  Some of these questions can be answered at this point, and some of them can be deferred.

When building microservices, there are many variables to consider. However, this is where software architecture shines – it involves making informed decisions while balancing different tradeoffs.

Keep in mind that good software architecture requires us to defer important decisions until we have a better understanding of the context. In the meantime, we can reason from a higher level of abstraction and use the available information to guide our decision-making process.

Successful implementation of good architecture depends on architects having a thorough understanding of the problem space and its constraints. So let's remain flexible and adaptable as we navigate this exciting journey!

I first shared this on my LinkedIn here. You can check it out, and, hopefully, connect with me. In the next part of this, I’ll touch on Distributed Systems concepts like Synchronous / Asynchronous IO, Messaging, Event Sourcing, SAGA, and much more. Ciao. 

Copy Link

Get more stories like this

Sign up for exciting updates on Moniepoint and how we’re powering business dreams.