TL;DR Contrary to the common belief, proper conceptual decomposition of large software solutions doesn't require any particular architecture patterns in place. Being service-oriented doesn't necessarily enforce any constraints on build or deployment - it's a matter of proper, highly aware design which can't be conducted in a correct way without proper understanding of concepts like: capability, boundary, identity & role exclusiveness.
Disclaimer 1: this is not a post about microservices, this one is about importance of design & proper decomposition of problems you're facing.
Sometimes these're the most "obvious" topics that cause the most confusion. The concept of service belongs exactly to this category - the word "service" is very frequently used in many work-related situations, but it seems that we (IT people) vary in terms of our perception & understanding of what service is (should be?):
- for some it's just an atomic business operation that persists its state between calls
- others claim that it's any chunk of functionality that can be injected via DI
- many follow the classic concept of service by SOA (never really codified within one, shared definition, so it's perceived rather intuitively)
- some zealots vouch for as much independence as possible, treating separate processes as a key requirement
I'm certainly not going to claim that I know the best definition, in fact I'm not even going to convince you to anything. The only goal here is to share the version of the concept of service that I personally find most beneficial in everyday's work. I encourage you to read through, confront it with your own beliefs/opinions & use your own judgement whether you can take anything beneficial out of it.
Disclaimer 2: I'll focus on concept of service in on-line systems/applications. For the sake of simplicity.
Why don't we try to define "service" with a list of striking one-liners that hopefully do not leave much to interpretation?
C1. Service is a design construct, not a code pattern.
Yes, we (should) start defining services even before making a decision of what kind of technology will be used for the sake of implementation.
Example: in this system some users will be booking the cinema tickets, some will update what are the forthcoming premieres, etc. As different concerns, these are candidates for separate services.
C2. Service is an abstract in model that represents the capability.
Probably the most crucial statement to understand the concept of service. In IT solutions actors (automated, human) have their capabilitites - sets of abilities they can perform. On the higher level these capabilities may together form roles, positions, etc. but basic capabilities are still there, under the hood. Capability ain't just a single action that can be performed - it's a set of actions (mutating or not) that are coherently oriented around some distinguished concept (& data that are depicting this concept only).
Example: cashiers have capability of issueing the ticket, clients have capability of creating a new order, offer managers have capability of adding new movies to the catalogue.
C3. Service'es capability has to be exclusive within (bounded) context.
Within a given bounded context (part of model that serves particular sub-domain) each capability should be laser-sharp: there can't be two parallel capabilities that manipulate the same information in a different manner - this would cause a risk on incoherence & watering down the responsibility of a service. If needed, slice your services more finely, but keeps each service's clear identity (so everyone knows that XYZService is a one-stop-shop to do stuff with XYZ).
Example: Imagine 2 services used to sell & issue cinema tickets - one is for on-line sale & the other one for Point-of-Sale one, if these 2 are designed & implemented separately, how do you make sure that the concept of ticket (in the end there's just a ticket in customer's "hand") maintains the same inherent qualities in both cases? Maybe the details of different sale process should belong to dedicated sales services (OnlineTicketSale & PosTicketSale) & issuing of tickets to another one (TicketIssuing)?
C4. The only part of Service visible outside it its contract (API - with the outside work), everything else is an implementation detail.
Everything else: implementation (algorithms, processing logic), data structures (data model, database content incl. business parameterisation), etc. The word "contract" is absolutely crucial here - service contract is both your agreement & commitment in the same time:
- agreement to serve particular functionality to all the consumers that conform to this service definition
- commitment that this service will be there when they need it to deal with the capability they need
Example: It does NOT matter that ticket DB table does have a column "valid until" as this one is NOT exposed in services contract -> it means that no-one but service implementation is allowed to manipulate it in any way. Service contract can expose this information in a another way (e.g. as "is still valid" property) - no-one (of the service consumers) should care about how it's implemented - it's the part of aggrement+commitment deal that you take what contract exposes for granted, as a single source of truth (for this capability).
C5. Services are building blocks of higher level capabilities (which are also Services)
Conceptually: atomic interactions, when orchestrated properly, build up more complex interactions like workflows & processes. This doesn't conflict with capabilities of these atomic interactions as they are not overwritten, neglected or even "enhanced" - higher level service just utilises them as they are.
Example: You can do various stuff with a single note (money one) - spend it, tear it down, roll into a ball, ... These are capabilities of a note (every note as it's a standardised object). Think about a wallet (a physical one) - it collects several notes - you can shuffle them around, sort them, etc. but to do such operations you still need to "call" atomic operations on individual notes (pull out 5 bucks note, align it so the dead president face is at the top, insert it in the beginning of the note stack).
C6. Capability exclusiveness doesn't equal to information exclusiveness
Data redundancy to the rescue! The same information can be represented (across whole system/platform) by several, separate physical representations - if you've heard about CQS/CQRS you probably already know what I mean: primary (transactional) service that performs writes can trigger system-wide events which will populate mutated data to many, specialized read-only data stores that power read-only services responsible for non-transactional capabilities (reporting, exports, analysis, etc.). This can also be covered by batch or native replication, etc.
Example: You've designed a system built around the concept of customer - customer has a lot of specific information oriented around different capabilities: his interest, connections, customisations, physical locations, market segments is qualifies to, etc. You want to create a heavy-duty dashboard with the most important 1-2 data quanta from all these capabilities - does it mean you have to refer to all these services separately?
Original services are still responsible for the original capabilities (& they remain origins of truth within their areas), but what they additionally do is publishing the event that particular piece of data has been updated, so some specialized read-only service (e.g. one responsible for dashboard only), that subscribe for such events can keep their information (separate, duplicated one) up-to-date. Such service's capability is ONLY to serve aggregated information for dashboard.
What's in it for me?
OK, these principles are just like any other principles - they look good on paper, they don't seem to contradict (hopefully), but what's the point, what's the gain?
The gain gets visible once you use service as a mean to tackle domain complexity: slicing domain into capabilities (hierarchically, until you get down to atomic, low-level ones) is a very flexible top-down decomposition method:
- you start with general, get down to specific - but only where & when you need it
- it doesn't assume any "artificial" component model - no business people speak about components, but they do speak about roles/functions/duties (aka capabilities)
- it doesn't force any particular implementation paradigm (OOP, FP, etc.), platform, protocol
- limiting interactions to contracts increases agility (everything else is an implementation detail, so can be swapped w/o breaking changes) & reduces dependencies' burden (you can expose contracts even before they are implemented)
- you don't need to split your application into physically separate (micro-)services (different processes) to implement services properly - it works exactly as well with monoliths (where services help with maintaining modularized internal structure)
Words of warning
The concept of service is deceptively simple, but the devil is certainly in the details. Building a good service is NOT an implementation challenge, it's definitely a design one (I can't emphasize it much enough!):
- services' contracts (hence service capabilities) have to be designed from (service) consumers' perspective (even if there are no consumers yet & provider is making a hypothesis)
- proper modeling has to happen first, to guarantee contract's stability - used domain terms (ubiquitous language), introduced domain invariants & contraints - it may be very hard to replace/modify them once consumers embrace them
- it's not easy & takes some experience to express service contract in a way that minimizes ambiguity & doesn't encourage various types of excessive coupling (e.g. behavioral) - good contract both constraints & leads by the hand
- if you downplay the importance of service design (e.g. services are based on use cases - "User Stories" - not on capabilities), hitback will be (eventually - sooner or later) painful - including: cyclic dependencies, excessive numbers of dependencies, massive service fragmentation, overlapping/unclear responsibilities (lack of service's "identity")
I work on building software products for nearly 20 years & if I had to name Top 5 challenges I've encountered during all my professional endeavors, scaling out the complexity & functionality (in terms of how many capabilities platform serves - on other words: how feature reach it becomes over the time) would clearly be on this list. In my personal opinion (I can underline that with full conviction), there's no better way to deal with this problem than hierarchical service/capability decomposition.