Chapter 13. Service-Based Architecture Style

A pragmatic hybrid of microservices: distributed, but without the cost and complexity of microservices or event-driven architecture. A popular choice for business applications that want modularity and some distribution benefits without paying the full microservices price.

Topology

A distributed macro-layered structure: a separately deployed user interface, a handful of separately deployed coarse-grained domain services, and a (usually) single monolithic database.

  • Services are coarse-grained “portions of an application” — independent, separately deployed like any monolith (EAR/WAR/assembly), no containerisation required (though Docker is fine). Because they share one database, the count typically runs 4–12 services (average ~7).
  • Usually a single instance per service, but multiple instances are possible for scalability/fault tolerance/throughput (needing load balancing between UI and service).
  • Accessed remotely from the UI, usually via REST (messaging, RPC, or SOAP also possible). An API layer (proxy/gateway) is optional; often the UI uses a service-locator pattern directly.
  • The centrally shared database lets services use SQL queries and joins like a traditional monolith. With so few services, connections aren’t usually an issue — but schema changes can be (see Database Partitioning).

Topology variants

One of the most flexible styles:

  • The single monolithic UI can be split into UI domains, even one per domain service.
  • The single database can be split into separate, even domain-scoped, databases (like microservices) — provided each database’s data isn’t needed by another service, to avoid inter-service communication (to be avoided here) and data duplication.
  • An API layer (reverse proxy/gateway) can sit between UI and services — useful for exposing functionality externally or consolidating cross-cutting concerns (metrics, security, auditing, discovery) outside the UI.

Service design and granularity

Each coarse-grained domain service is usually designed internally as a layered architecture (API facade → business → persistence), or domain-partitioned into sub-domains like a modular monolith. Either way it needs an API access facade that the UI calls and that orchestrates the business request internally.

Example: a “place order” request hits the OrderService facade, which internally orchestrates placing the order, generating an order ID, applying payment, and updating inventory — all class-level orchestration within one service. In microservices, the same would require orchestrating many separately deployed services. This is one of the big granularity differences between the two styles.

Because services are coarse-grained, they use regular ACID transactions (commit/rollback) for integrity within a single service. If a catalog checkout fails on an expired card, everything rolls back atomically. The microservices equivalent — OrderPlacement inserts the order, then calls PaymentService which fails — leaves data in an inconsistent state (order inserted but unpaid; what about inventory?), the kind of question fine-grained distribution forces you to answer. The trade-off cuts both ways: a change to order placement in service-based architecture requires testing the whole coarse service (including payment), with more code deployed and more risk; in microservices the same change touches only a small OrderPlacement service.

Database partitioning

Services usually share one database, so a table schema change can ripple to every service — a costly coordination problem. The shared table-schema classes (entity objects) live in a shared library (JAR/DLL), possibly with SQL.

  • Single shared library of all entity objects — the least effective approach. Any table change forces a change and redeploy of every service, whether or not it touches that table, and it’s hard to know which services are actually affected without manual analysis.
  • Federated shared libraries — logically partition the database into domains (e.g. common, customer, invoicing, order, tracking) with one shared library each. A change to an invoicing table now only impacts services using the invoicing library. A common domain library used by all services is normal; mitigate changes to it by locking the common entity objects in version control and restricting change to the database team.

Tip: make the logical database partitioning as fine-grained as possible while keeping well-defined data domains, to better control database changes.

Example architecture

An electronics-recycling system with domains implemented as separately deployed services: Quoting, Receiving, Assessment, Accounting, ItemStatus, Recycling, Reporting. Only the customer-facing Quoting and ItemStatus services need to scale; the rest run single instances. The UIs are federated into Customer Facing, Receiving, and Recycling and Accounting domains (giving UI fault tolerance, scalability, and security). Two physical databases — external customer-facing and internal — sit in separate network zones with one-way firewall access (internal can read/update customer-facing, not vice versa), improving security. The frequently-changing Assessment service shows the benefit: changes are isolated to one domain service, delivering agility, testability, and deployability.

When to use this style

The flexibility plus the many three- and four-star ratings make this one of the most pragmatic styles. More powerful distributed styles exist, but some companies find that power too expensive, or simply don’t need it — like driving a Ferrari to work in rush-hour traffic.

  • Natural fit for DDD — coarse-grained, domain-scoped services map cleanly onto domains, compartmentalising functionality and easing change.
  • Best ACID preservation of any distributed style — coarse services keep most transactions within a single service (traditional commit/rollback). When the UI or API gateway orchestrates two-plus services, you fall back to sagas/BASE.
  • Good modularity without granularity pain — fine-grained services force you into orchestration (a central mediator coordinating a transaction, like a conductor) and choreography (services talking peer-to-peer, like dancers). Coarse-grained services need this coordination far less.

Trade-offs

A domain-partitioned architecture (structure driven by domain, not technical concern). Being distributed, the number of quanta is ≥ 1: 4–12 services sharing one database/UI form a single quantum, but federating UI and database yields multiple quanta (the recycling example has two — customer-facing and internal operations).

Characteristic ratings (1 = poorly supported, 5 = defining strength):

CharacteristicRating
Agility4
Testability4
Deployability4
Fault tolerance4
Availability4
Reliability4
Scalability3
Elasticity2
SimplicityHigh (relative to other distributed styles)
CostLow (relative to other distributed styles)
Number of quanta≥ 1

Reasoning:

  • No five-star ratings, but strong (4) across many vital areas. Splitting into domain services gives faster change (agility), narrower test scope (testability), and more frequent, lower-risk deploys (deployability) — together improving time-to-market.
  • Fault tolerance & availability rate 4 because services are self-contained and avoid inter-service communication (thanks to shared DB/code), so one service going down doesn’t take others with it.
  • Scalability is 3 and elasticity only 2 due to coarse-grained services replicating more functionality than fine-grained ones — less resource-efficient. Typically single instances unless throughput/failover demands more.
  • Simplicity & cost are key differentiators — the easiest and most cost-effective distributed style. The trade-off: the four-star ratings would climb higher with more cost/complexity.
  • Reliability beats other distributed styles because coarse services mean less network traffic, fewer distributed transactions, and less bandwidth.

Source

Mark Richards & Neal Ford, Fundamentals of Software Architecture, Chapter 13.