Drawn/blueprint-style illustration of the service-based architecture pattern.

In our previous post we looked at a technically partitioned, coarse-grained, distributed architecture. Although that pattern balances capabilities like scalability and elasticity with low cost and simplicity, a domain-partitioned architecture may offer many advantages. For an organization with well-defined domains and domain-aligned teams, this next pattern offers a very balanced and pragmatic distributed architecture that offers comparable capabilities to an architecture like microservices at a fraction of the cost and complexity. In short, a Service-Based architecture offers a highly viable “middle-ground” between monoliths/n-tier architecture and microservices. This architecture can act as a stepping stone between monoliths and more complex topologies although often it will represent an acceptable and desirable end-state for green-field and legacy systems alike. Read on to learn about the Service-Based Architecture

This post is part of a series on Tailor-Made Software Architecture, a set of concepts, tools, models, and practices to improve fit and reduce uncertainty in the field of software architecture. Concepts are introduced sequentially and build upon one another. Think of this series as a serially published leanpub architecture book. If you find these ideas useful and want to dive deeper, join me for Next Level Software Architecture Training in Santa Clara, CA March 4-6th for an immersive, live, interactive, hands-on three-day software architecture masterclass.

Introducing the Service-based Architecture Pattern

Some architects call this pattern “service-based,” others have adopted the moniker “miniservices” as they are typically much more coarse-grained (less “micro”) than microservices. This pattern typically consists of several domain-centric deployable services (rather than the hundreds or thousands that might exist if the same system were deployed as fine-grained microservices). This is a very common pattern in the wild with some projects arriving styles derived from this pattern by design, many others end up here out of necessity.

“90% of organizations who try to adopt microservices will fail, they will find the paradigm too disruptive and often switch to miniservices” Gartner

At a high-level, this pattern typically consists of an independently deployable user interface, independently deployable coarse-grained services (exposing some kind of API), and a monolithic database.

Like all domain-partitioned architectures, this architecture starts with domain-driven understanding of the problem-space to be supported by the system which informs potential module boundaries. As an architect, you have ultimate control over individual component granularity, although typically module boundaries will initially break along domain boundaries (often referred to as “domain services”). Even with coarse gained components, most changes are scoped to a single domain which accelerates software delivery and improves overall agility.

In the abstract style, a single, monolithic database is shared among services and, although this increases coupling, it eliminates the complexity of splitting databases as well as all but eliminating the need for orchestrating distributed transactions and the overhead application-side joins. Of course additional constraints such as a partitioned shared database or federated database reduce coupling and improve scalability, in many cases this architecture offers “enough” of any given capability for most cases. While database coupling is a deliberate trade-off in this style, some code coupling is also common in the form of shared, versioned code libraries over duplication.

This abstract style doesn’t prescribe how code is structured inside any given domain service. If migrating from a layered monolith, it may be most expedient to extract a vertical slice of the monolith at roughly the domain boundary, creating layered code within each domain service (at least as as starting point). For greenfield development (or if you are decomposing a modular monolith or in the later stage refactoring of a layered monolith) it may be preferable to structure the internals of each domain service similarly to the modular monolith. This approach opens up an axis of future flexibility should it make sense to ultimately decompose the system into microservices or if, long term, the system would benefit from being further decomposed into “multigrain” services (some course-grained, some fine-grained as it makes sense).

Core Constraints

The overall capabilities of this abstract style can be depicted as follows:

Illustration of the service-based architecture, defined as "Well-defined domains deployed as separate coarse-grained components." with a graph of strengths and weaknesses

These capabilities are induced by a number of core constraints as follows:

  • Medium-Grained Components
  • Independent Deployability
  • Domain Partitioning
  • Shared Database
  • Separation of Concerns
  • API communication

There are many strategies and approaches to API communication, each with various strengths, weaknesses, and trade-offs. We will presume a RPC-style API for simplicity.

Constraint: Medium-Grained Components

This constraint states that the overall system is decomposed into medium-grained components. Generally these components are scoped to a single business domain.

Elasticity (+3)

Decomposed components are generally lighter-weight and offer reduced start times eliciting improved elasticity compared to monolithic systems or coarse-grained components.

Fault-tolerance (+3)

Independent components of any granularity begin to reduce the operational cost of running any given component. It becomes feasible to run multiple instances of a component which reduces the risk of system-wide failures when any individual instance of a component fails. Furthermore, components generally represent only a subset of the overall system functionality which supports implementation decisions that enable the majority of the system to continue functioning.

Performance (+2.5)

Medium-grained components generally require fewer network calls as many common functions are co-located locally within the component boundaries reducing latency and improving performance.

Deployability (+2)

A system decomposed into compents paves the way for smaller-scoped deployments and partial deployments of the system. Deployments scoped to components, reduces deployment risk and simplifies pipelines.

Scalability (+2)

Smaller components may be individually scaled increasing total available compute where it is most needed.

Testability (+2)

>Breaking an application into smaller components reduces the total testing scope and medium-grained components are usually split at the domain boundary reducing inter-component testing scope.

Agility (+1.5)

Decomposing a system into independent components of any granularity with thoughtful module boundaries has the potential to improve business agility. Components may be independently evolved and deployed with reduced overall testing scope.

Evolvability (+1.5)

Decomposition into components enables different parts of the system to evolve at different rates. Adding or changing functionality may often be scoped to a single component.

Integration (+1)

Medium-grained components integrate well within the domain boundary, although crossing domains may introduce challenges.

Interoperability (+1)

Decomposing as system into components is a forcing function for integration. Components must be able to communicate and work together, which generally improves this capability at the system level.

Configurability (+0.5)

Decomposing a system allows components to be individually configured and each component may utilize different framework versions, runtimes, languages, or platforms.

Cost (+0.5)

Medium granularity of components reduces total cost of ownership in terms of development time, change time, and bandwidth costs but is offset by design, development, and compute overhead involved in a distributed system.

Workflow (+0.5)

Medium-grained components typically contain many modules and functions grouped by domain which modestly improve the ability for a module to orchestrate workflow behavior within a module boundary.


Constraint: Independent Deployability

This constraint states that the system as a whole need not be delpoyed at one time. Any individual component must be deployable in isolation. This consrtaint cannot co-exist with the monolithic deployment granularity constraint.

Agility (+1.5)

Independent deployability requires some rules around modularity. Clean and stable module boundaries must, therefore, exist (be it in the form of plug-in architecture, event processors connect by async processing channels, independant services, or service layers). These module boundaries in place, combined with independent build and deployment pipelines, significantly improve overall agility. To respond to change, a module can be create or modified and deployed with low overall risk to the rest of the system. Change scope remains constrained and deployment risk reduced increasing overall delivery velocity.

Deployability (+1)

In the same vein of Agility, overall deployability is improved.

Elasticity (+1)

Independent deployability improves component independence and improves elasticity.

Evolvability (+1)

The independant deployability constraint enables different parts of the system to evolve at different rates. Adding or changing functionality is often as simple as a single, scoped, focused deployment.

Scalability (+1)

Independent deployability improves component independence and improves scalability.

Cost (+0.5)

The capability of surgically-precise deployments reduces the overall total cost of ownership of the system. This capability is limited, however, as independant deployability typically requires additional investment in build and deployment infrastructure which incurs a cost penalty. In aggregate, the trade-offs of this constraint are a small net positive.

Simplicity (+0.5)

Although development and management of independant build and deployment pipelines introduce complexity and require some specialized skills, once this infrastructure is in place, the system as a whole is generally easier to maintain and modify. Again, in aggregate, the trade-offs evaluate to a modest net positive.


Constraint: Technical Partitioning

This constraint introduces some structure in terms of how components of the system are organized. In this case components are grouped by their technical categories. As the depiction of the abstract style indicates, usually these are along the line of UI/Presentation, Business logic, persistence, and database but this constraint can apply to both monolithic and distributed topologies.

Generally layers are consider either open or closed. Closed layers abstract any layers below the closed layer - meaning they must act as an intermediary. Open layers are free to be bypassed when it makes sense (the sinkhole antipattern defines a scenario where the layers don’t apply any meaningful changes or validation on the data as it passes through a layer).

Cost (+2)

This constraint introduces some structure in terms of how components of the system are organized. In this case components are grouped by their technical categories. As the depiction of the abstract style indicates, usually these are along the line of UI/Presentation, Business logic, persistence, and database but this constraint can apply to both monolithic and distributed topologies.

Generally layers are consider either open or closed. Closed layers abstract any layers below the closed layer - meaning they must act as an intermediary. Open layers are free to be bypassed when it makes sense (the sinkhole antipattern defines a scenario where the layers don’t apply any meaningful changes or validation on the data as it passes through a layer).

Testability (+0.5)

Because this constraint add some structure to our software components, testing scope becomes better defined as do interfaces between layers.

Abstraction (+0.5)

Because one layer must interact with another, often this results in better interfaces and abstractions being put in place. As a result, generally this constraint slightly improves abstraction within the system.

Deployability (-1)

Deployability is degraded here, whether this constraint is applied to a monolithic or distributed technically partitioned system. Generally any single change requires modifications to all layers which increases testing and regression-testing scope and reduces velocity while increasing deployment risk.

Configability (-2)

Technical partitioning might introduce tight coupling between different parts of the application. This can make it difficult to change or configure one part without affecting others.

Evolvability (-2)

The large change scope, reduced deployment velocity, and increased change risk also adversely affects evolvability. The risk surface area is much larger than in domain-partitioned systems.


Constraint: Shared Database

This constraint states that the entire application utilizes a common database. While this is often a default of some abstract styles and patterns, it still strengthens and weakens capabilities and should be explicitly noted.

Cost (+1.5)

Generally sharing a single, shared database reduces licensing costs, hosting costs, and reduces development costs. Generally this also reduces data storage redundancy as there is much less need to replicate data to be visible to other application components.

Simplicity (+1)

Administration is simplified by virtue of having a single database to manage. Design is also simplified as all data modeling can be done at the application level rather than domain level.

Deployability (+0.5)

Deployment is generally straightforward as changes to a single database have reduced coordination costs. The improvement is modest, however, as DB changes at this scale can affect availability and introduce risk if schema that other components rely on change.

Configability (-0.5)

Configurability is reduced as any changes must be applied system-wide. A one-size-fits-all approach is generally required under this constraint.

Fault-tolerance (-0.5)

A single database becomes a single point-of-failure. Although most database management systems bring high-availability configuration options, if one database (the only database) is unavailable, the entire system is unavailable.

Scalability (-0.5)

Databases are notoriously difficult to scale. Multiple databases responsible for different parts of the data provide some level of parallelism and increase total capacity, a single database may be limited to scaling up.

Agility (-1)

Database changes potentially require coordinating with all teams and must be regression tested across all components. It can be very difficult to tell which teams are using various tables. Consequently, any change introduces risk which reduces change velocity.

Evolvability (-1)

The high coordination cost and testing scope also degrades evolvability.

Elasticity (-2.5)

As a single, shared resource, the system as a whole becomes less elastic because there is a ceiling to the single database’s capacity.


Constraint: Separation of Concerns

This constraint further narrows the technical partitioning constraint by being more prescriptive around how layer boundaries and modularity are defined. This constraint defines that code is not simply defined by technical area, but also logical concern.

Cost (+2)

Development and maintenance costs are reduced by adding this level of modularity to code. Developers may develop deep domain expertise in business logic (or subset of the business logic) which further reduces cost.

Testability (+1)

This constraint further reduces testing scope for any given change.

Agility (+1)

Agility is improved as the code generally has better boundaries, reduced testing scope, and potentially change scope.

Simplicity (+1)

This constraint generally improves simplicity of development and maintenance of the code. It is well-defined way to develop software, and this constraint improves understandability of the system components as well.

Evolvability (+0.5)

Evolvability is slightly improved as a consequence of the factors detailed above.


Constraint: RPC-API

This constraint states that network communication between components take place through an remote-procedure-call (RPC) application programming interface (API)

Configurability (+1.5)

An API provides configurability through versioned contracts, the potential for more fine-grained control over signatures and payloads, and potentially content-negotiation.

Fault-tolerance (+1)

Networked API calls enable real-time routing to API endpoints. Such calls may often be load balanced and load-balancers may be aware of API instance health. When API interactions are semantically safe and/or idempotent, clients are free to retry failed requests. RPC APIs may lack standard response codes and clients may not know which requests are idempotent hampering failure recovery of some requests.

Integration (+1)

APIs provide a well-defined interface for component integration.

Interoperability (+1)

APIs provide a well-defined interface for component interoperability.

Cost (+0.5)

APIs, even RPC APIs (which are often tightly coupled) standardize on an interface which reduces development and maintenance costs.

Elasticity (+0.5)

Networked API calls may be routed to multiple available endpoints improving potential elasticity.

Performance (+0.5)

RPC endpoints are generally highly specific which enables some optimizations. Although coupling is often increased as a trade-off.

Scalability (+0.5)

Network APIs improve overall system scalability slightly.

Abstraction (-0.5)

The highly-specific nature of RPC endpoints reduces API abstraction.

Deployability (-0.5)

The highly-coupled nature of RPC endpoints often require coordinated changes reducing overall deployability.

Evolvability (-0.5)

The highly-coupled nature of RPC endpoints make changes more difficult.

Agility (-1)

The difficult of changing APIs without frequent client coordination reduces overall agility.

Summary

Overall this pattern offers a broad set of good-to-above-average capabilities without introducing too much cost and complexity. This pattern is useful both as a target or stepping stone as part of an architectural modernization effort or as a design choice for a new system. Although no capability maxes out in terms of scoring, the Tailor-Made approach encourages designing “enough” of a capability into the system as, very often, “too much” or “a lot” can come at a very high cost that often simply isn’t worth paying.