Evaluation Criteria - A Guide to Service based Architectural Styles

Posted on 2025-02-10
architecture service-oriented-architectures microservices monoliths
Architectural patterns such as Micro-frontends, Backend for Frontend, Experience Services, and Domain Services are evolutions of service separation and specialization a.k.a. service-oriented architectural style (Read more in my previous blog post: Service Oriented Architecture - Slice and Dice about what these patterns are with examples). These patterns are designed to address specific challenges. Just like with any decision we make in software engineering, when we choose to use these patterns over other ways of solving the challenge, we need to understand the benefits, costs, and key enablers of these patterns. This understanding allows us to create a lay of the land and understand the trade-offs we are making.
Let's lay it out!
Before further reading, a few quick notes:
  • You may notice I have added modular monoliths into the mix. This is because modular monoliths are a great starting point, even if your organisation eventually wants to move to microservices. A monolith can help kickstart and test the waters.
  • All the architectural patterns that have evolved from microservice style have certain common benefits, costs, and enablers. This is because of the service-orientated nature of these patterns.
1. Modular Monoliths (Note: The 'Modular' prefix in 'Modular Monoliths' is used to steer clear of a spaghetti/legacy codebase.)
Benefits Associated Costs Enablers
  • Simpler deployment and operations: A monolith is a single unified codebase, which makes it easier to deploy and operate.
  • Easier troubleshooting: You have exactly 1 codebase to deal with; chuck in a few debug points, and you are good to go.
  • Simpler starting points: It is always faster to spin up a single application and manage it when getting started. Monolith first creates a focus on the problem without the overhead of operational complexity.
  • Simpler developer machine setups: Checkout the code, follow the README for setup, and run the local server. No overhead of setting up mocks, stubs, or running multiple services.
  • Cost-effective: Since operational complexity is low, the cost of running a monolith is often lower than running multiple services.
  • Easier to manage transactions: When a series of operations need to be performed as a single unit i.e. a transaction, monoliths are easier to manage these. Managing a transaction across multiple services is nothing short of a nightmare.
  • No/Limited diversity in technology: Monoliths are written in a single programming language and framework.
  • Everything gets deployed together: Monoliths are deployed as a single unit. Often with monoliths achieving zero downtime deployments with techniques like blue-green, canary deployment, etc. is a challenge. Release trains are a common pattern to manage deployments.
  • Vertical scaling only: As the size of monoliths grows, the way to scale is often vertical i.e. increasing the size and resources of the server. While some horizontal scaling can be achieved it is limited.
  • Slower build times: As the codebase grows, the build times tend to increase. Teams often invest in techniques to build specific modules only though this can be challenging and technique is dependent on tech stack.
  • Physical division of boundary is limited to folder structure: Since we are only taking in account modular monoliths, I am assuming the team(s) have done domain modeling before understanding what goes in which modules. The division of boundaries by modules is achieved by folder structure.
  • Domain modeling: Understanding the domain and performing domain modeling activities is a crucial enabler. This understanding allows the team to create modules which are cohesive within and loosely coupled to each other. Specifically if medium to large sized organisations want to adopt the approach.
  • Ways of working (WoWs): Clearly defined ways of working is another key enabler. Some examples are as follows:
    • Definition team/squad remits and custodianship of parts to allow working on the monolith in parallel.
    • Handling broken builds and not letting teams commit to the main branch until the build is green again.
    • Code conventions, linting rules, and editor configurations to ensure consistency across the codebase.
    • Code reviews and pair programming to ensure quality of code.
  • Test automation: Having a comprehensive test suite, including unit tests, integration tests, and end-to-end tests allows multiple team to make changes confidently without impacting functionality owned by other teams.
2. Micro-frontend
Benefits Associated Costs Enablers
  • Strong and enforced module boundaries: Micro-frontends are aligned to domains e.g. Team Product would own the product micro-frontend, Team search would own the search micro-frontend. These boundaries are enforced by separation of codebases and infrastructure.
  • Independent deployments: Each micro-frontend can be deployed independently via dedicated CI/CD pipelines. This allows teams to deploy their changes without any dependencies on other teams.
  • Technology diversity: Teams have autonomy to choose programming languages and frameworks that suits their needs and/or capabilities.
  • Autonomous teams: The pattern and its benefits enable teams to be autonomous in their decision-making and this in turn enables faster mechanisms to deliver value to customers.
  • Multiple versions of same library or too many frameworks: Teams may end up using different versions of the same library or too many frameworks. This can lead to performance impacts on the cohesive website.
  • Inconsistent user experience: Teams can end up creating inconsistent user experiences if proper tools, conventions and communication are not in place. Different colour schemes, different fonts, different components will lead to a disjointed experience.
  • Inter-frontend communication: Micro-frontends need to communicate with each other. For user experience to be cohesive, when user interacts with one micro-frontend, the other micro-frontend might require to be updated. While there are techniques each technique has its own trade-offs.
  • Increased network calls and payload size: Each micro-frontend requires to make network calls and fetch data. This can lead to increased network traffic from the website and increased payload size. Often the "Backend For Frontend" a.k.a BFF pattern is used to mitigate this.
  • Increased operational overhead: The number of services can grow drastically when pattern starts getting adopted across the organisation. This would require more operational overhead to manage these services, and a mature core platform team to help with that.
  • Design system library: To support consistent user experience, a design system library can be created. This library is a collection of reusable components, styles, and guidelines. In addition to this, the library also enables a faster way to create experiences within the micro-frontends by providing a set of pre-built components.
  • Strong and proactive FE community: Autonomy has its benefit though can also lead to silos, and teams not sharing knowledge. This manifests in how repositories shape as well as slower delivery as teams are not leveraging each other's work. A strong and proactive FE community can help in sharing knowledge, best practices, and tools.
  • Shared tools and plugins: Logging, monitoring, authorisation, linting, testing, etc. are common concerns each micro-frontend would need to address. Having shared tools and plugins (either open source, or build specific to need of organisation) can help in reducing the overhead of each team building these tools. As an enabler, this allows team to focus on delivery at hand, help newer frontends to onboard faster, and security updates to these tools can be managed centrally.
  • Lightweight governance: As mentioned earlier, while autonomy brings in much-needed freedom, and ability to deliver faster, it can also lead to an eventual chaos. Under light-weight governance autonomy can flourish. Light-weight governance is a set of guardrails in form of principles, patterns and sensible defaults. A few examples are as follows:
    • Tech Radar is a great example of tool used for light-weight governance.
    • Restricting languages and frameworks to a set of approved ones. This allows teams to still have a choices, and the company to have a way to avoid supporting too many languages and frameworks.
    • For internally developed packages and dependencies, asking teams to keep major versions in sync with the released version.
  • Self-service platform: A team that would want to create a new micro-frontend should be able to do so without too much dependency on other teams to stand up the infrastructure. A self-service platform is offered by the core platform team within the organisation. The platform leverages Infrastructure-as-Code (IaC) to allow teams to spin up (or create) environments, CI/CD pipelines, observability, etc. around their micro-frontend.
3. Backend for Frontend (a.k.a. BFF)
Benefits Associated Costs Enablers
  • Independent deployments: Each BFF can be deployed independently via dedicated CI/CD pipelines. This allows teams to deploy their changes without any dependencies on other teams.
  • Technology diversity: Teams have autonomy to choose programming languages and frameworks that suits their needs and/or capabilities.
  • Autonomous teams: The pattern and its benefits enable teams to be autonomous in their decision-making and this in turn enables faster mechanisms to deliver value to customers.
  • Reduced network calls and optimised payload size: BFF patterns either align with micro-frontends or align with digital channels. In both cases (though specifically the latter), the BFF pattern allows reduced networks calls directly from the browser and additionally send back the optimised payload the frontend needs.
  • Reduces complexity in frontend codebases: The BFF pattern allows frontend codebases to be simpler and focused on user experience components. It achieves so by moving aggregation and optimisation logic to BFF layer.
  • Can lead to over-engineering: Teams should create a BFF only when there is a need. Micro-frontends or certain channels may not need too much aggregation or payload optimisation, and thus may not require a BFF.
  • Code duplications: Specifically when BFFs are channel aligned, they can end up with a lots of duplication. There are mitigation strategies such as - shared libraries, and/or experience services.
  • Increased operational overhead: The more the number of services we have the more the operational overhead and costs to manage and control. As mentioned previously, BFF is not a must-have pattern for every micro-frontend or channel. It should be used when there is a need to keep check of ever-increasing number of services.
  • Service discovery and communication: BFFs require to talk to domain services, and experience services. A crucial enabler which allows for secure and efficient communication is supporting service discovery and easy ways to facilitate east-west communication. Service-mesh for kubernetes is a great example for this.
  • Shared tools and plugins: Logging, monitoring, authorisation, linting, testing, etc. are common concerns each micro-frontend would need to address. Having shared tools and plugins (either open source, or build specific to need of organisation) can help in reducing the overhead of each team building these tools. As an enabler, this allows team to focus on delivery at hand, help newer frontends to onboard faster, and security updates to these tools can be managed centrally.
  • Lightweight governance: As mentioned earlier, while autonomy brings in much-needed freedom, and ability to deliver faster, it can also lead to an eventual chaos. Under light-weight governance autonomy can flourish. Light-weight governance is a set of guardrails in form of principles, patterns and sensible defaults. A few examples are as follows:
    • Tech Radar is a great example of tool used for light-weight governance.
    • Restricting languages and frameworks to a set of approved ones. This allows teams to still have a choices, and the company to have a way to avoid supporting too many languages and frameworks.
    • For internally developed packages and dependencies, asking teams to keep major versions in sync with the released version.
  • Self-service platform: A team that would want to create a new BFF should be able to do so without too much dependency on other teams to stand up the infrastructure. A self-service platform is offered by the core platform team within the organisation. The platform leverages Infrastructure-as-Code (IaC) to allow teams to spin up (or create) environments, CI/CD pipelines, observability, etc. around their micro-frontend.
4. Experience Services/APIs
Benefits Associated Costs Enablers
  • Reuse aggregation or orchestration logic: Different channels present meaningful user experience by aggregating data from multiple domains. Due to different stacks and technologies for different channel these types of logic often gets duplicated in every channel. BFFs do not truly solve this problem either. Another solution is to often push this aggregation logic to domain layer, the problem though is it breaks the domain boundaries. Experience layer create an anti-corruption layer to avoid this spillage yet DRY out the duplicated logic.
  • Independent deployments: Each Experience Service/API can be deployed independently via dedicated CI/CD pipelines. This allows teams to deploy their changes without any dependencies on other teams.
  • Technology diversity: Teams have autonomy to choose programming languages and frameworks that suits their needs and/or capabilities.
  • Autonomous teams: The pattern and its benefits enable teams to be autonomous in their decision-making and this in turn enables faster mechanisms to deliver value to customers.
  • Reduces complexity in frontend codebases: The Experience Service pattern just like the BFF pattern allows frontend codebases to be simpler and focused on user experience components. It achieves so by aggregator pattern, or orchestration pattern, or event collaboration pattern encapsulated in the experience layer.
  • Can lead to over-engineering: Teams should create an Experience service only when there is a need. Not all experiences are convoluted or require de-duplication across channels. Example:
    • It is probably a great idea to extract out payment experience which supports split payments, and multiple payment methods.
    • We are overdoing by simply extracting experience layer which manages (CRUD) project list.
  • Extra network hop and potential performance impact: Experience layer is yet another network hop. While performance can be a benefit of this pattern where it can aggregate data from multiple services, optimise payload and reduce network call originating from the browser; it could also be a cost if not done right. When experience layer is just a pass-through layer, it can lead to performance impact.
  • Risk of becoming a monolith: An experience service should ONLY be around a single experience. When (which is often) this boundary is not respected, the experience service can become a monolith (and probably not the modular monolith we referred to).
  • Increased operational overhead: The more the number of services we have the more the operational overhead and costs to manage and control. As mentioned previously, experience services are not a must-have pattern for every experience. It should be used when there is a need to keep check of ever-increasing number of services.
  • Encompass One and only one experience: Clear boundaries much be established and documented along with the codebase (e.g. in README). Ensuring this type of service is only built around a single experience is crucial. You do this by:
    • Service should encapsulate exactly one experience
    • Clear boundaries to not allow domain logic to leak in.
  • Clear division of responsibilities between layers: There is a chance that certain logic gets duplicated in frontend, experience and domain layer. For example, validation of inputs can be done in all three layers. Such duplication has high cost of change. Clear division of responsibilities between layers can help in reducing this duplication.
  • Service discovery and communication: Experience services require to talk to other experience services and/or domain services. A crucial enabler which allows for secure and efficient communication is supporting service discovery and easy ways to facilitate east-west communication. Service-mesh for kubernetes is a great example for this.
  • Shared tools and plugins: Logging, monitoring, authorisation, linting, testing, etc. are common concerns each experience-service would need to address. Having shared tools and plugins (either open source, or build specific to need of organisation) can help in reducing the overhead of each team building these tools. As an enabler, this allows team to focus on delivery at hand, help newer frontends to onboard faster, and security updates to these tools can be managed centrally.
  • Lightweight governance: As mentioned earlier, while autonomy brings in much-needed freedom, and ability to deliver faster, it can also lead to an eventual chaos. Under light-weight governance autonomy can flourish. Light-weight governance is a set of guardrails in form of principles, patterns and sensible defaults. A few examples are as follows:
    • Tech Radar is a great example of tool used for light-weight governance.
    • Restricting languages and frameworks to a set of approved ones. This allows teams to still have a choices, and the company to have a way to avoid supporting too many languages and frameworks.
    • For internally developed packages and dependencies, asking teams to keep major versions in sync with the released version.
  • Self-service platform: A team that would want to create a new experience service/api should be able to do so without too much dependency on other teams to stand up the infrastructure. A self-service platform is offered by the core platform team within the organisation. The platform leverages Infrastructure-as-Code (IaC) to allow teams to spin up (or create) environments, CI/CD pipelines, observability, etc. around their micro-frontend.
5. Domain Services/APIs
Benefits Associated Costs Enablers
  • Strong and enforced module boundaries: Domain-aligned services/APIs as the name suggests are aligned to domains e.g. Team Product would own the product domain service, Team search would own the search domain service. These boundaries are enforced by separation of codebases and infrastructure.
  • Independent deployments: Each BFF can be deployed independently via dedicated CI/CD pipelines. This allows teams to deploy their changes without any dependencies on other teams.
  • Technology diversity: Teams have autonomy to choose programming languages and frameworks that suits their needs and/or capabilities.
  • Autonomous teams: The pattern and its benefits enable teams to be autonomous in their decision-making and this in turn enables faster mechanisms to deliver value to customers.
  • High reusability: Domain services implement repeatable business activities contained within a domain. These activities lead to specific outcomes and are often used across multiple experiences. Encapsulated within a domain service these activities allow reusability without code duplication.
  • Increased operational overhead: The more the number of services we have the more the operational overhead and costs to manage and control. As mentioned previously, experience services are not a must-have pattern for every experience. It should be used when there is a need to keep check of ever-increasing number of services.
  • Eventual consistency: Maintaining strong consistency across a distributed system is a challenge. Eventual consistency implies when an update is made it would eventually reflect in the data. The trade-off of scalability and high availability guaranteed by this style of architecture is eventual consistency. Monoliths on the contrary have the ability to offer strong consistency by the means of transactions and synchronous operations.
  • Complexity in observability: It is often challenging to trace the causes of a user action on a website which spans across multiple services. Some of these domain services may get a synchronous call whereas others may take actions asynchronously listening to events. Observing a software system i.e. logging, monitoring, tracing, etc. can be challenging if we do not have the needed defaults and tools.
  • Always start with domain modeling: Having clarity of Domain Driven Design (DDD) principles and ability to model the domain is the basic building block for domain services. There is no way to get domain services right any other way.
  • Communication patterns and platforms: Domain services require to talk to other domain services either synchronously or asynchronously. As an organisation, having patterns, tools and platforms to facilitate this is a crucial enabler. Few key considerations are:
    • API and contracts discoverability
    • Conventions around versioning, structure, and naming
    • SDKs and client libraries
    • Tools such as eventing platform, service mesh, etc.
  • Service discovery and communication: Experience services require to talk to other experience services and/or domain services. A crucial enabler which allows for secure and efficient communication is supporting service discovery and easy ways to facilitate east-west communication. Service-mesh for kubernetes is a great example for this.
  • Shared tools and plugins: Logging, monitoring, authorisation, linting, testing, etc. are common concerns each experience-service would need to address. Having shared tools and plugins (either open source, or build specific to need of organisation) can help in reducing the overhead of each team building these tools. As an enabler, this allows team to focus on delivery at hand, help newer frontends to onboard faster, and security updates to these tools can be managed centrally.
  • Lightweight governance: As mentioned earlier, while autonomy brings in much-needed freedom, and ability to deliver faster, it can also lead to an eventual chaos. Under light-weight governance autonomy can flourish. Light-weight governance is a set of guardrails in form of principles, patterns and sensible defaults. A few examples are as follows:
    • Tech Radar is a great example of tool used for light-weight governance.
    • Restricting languages and frameworks to a set of approved ones. This allows teams to still have a choices, and the company to have a way to avoid supporting too many languages and frameworks.
    • For internally developed packages and dependencies, asking teams to keep major versions in sync with the released version.
  • Self-service platform: A team that would want to create a new experience service/api should be able to do so without too much dependency on other teams to stand up the infrastructure. A self-service platform is offered by the core platform team within the organisation. The platform leverages Infrastructure-as-Code (IaC) to allow teams to spin up (or create) environments, CI/CD pipelines, observability, etc. around their micro-frontend.
Rounding it up!
Having a layout of benefits, and costs allows to understand the trade-offs you are making. Enablers allow you to be prepared for the adoption of the pattern. Along being prepared, we should also be prepared for scaling of these patterns. A few successful adoption can open up floodgates and lead to a fair number of services. Enablers are needed for the scale-up as well. Though a thing to note is you can iterate on these enablers, you do not need everything up-front. This can help you buy time to understand if the pattern is working for you or not.
I would recommend when venturing out in direction of service-orientation or trying out any of these new platforms layout a plan. The plan should include: your current state, desired state, certain milestones and what enablers you need on your way. A small upfront investment can save you a lot of time and effort in the long run.