Suppose there is a generic Notification Service through which every notification is delivered.
There are two obvious ways to integrate it with the rest of the system, plus an evolutionary alternative based on mapper contexts.
The key question is not whether services may know about one another. Messages and APIs are contracts, so cross-boundary coupling is expected. The important question is where the business knowledge belongs.
For example:
Order Service
│
│ OrderCreated
▼
Event Bus
│
▼
Notification Service
The Notification Service would then need rules such as:
OrderCreated → notify
OrderFulfilled → don't notify
OrderCancelled → notify
That is the problem.
The Notification Service now needs to understand Order-domain semantics in order to decide which events should result in notifications.
It has to know things such as:
- why
OrderCreatedmatters; - why
OrderFulfilleddoes or does not matter; - which business conditions change the decision;
- which order events are relevant to customers.
That knowledge belongs with the Order domain. A generic Notification Service should not become a consumer of all those domain-specific decisions.
The issue is therefore not that Notification Service reacts to an event. A service can quite legitimately react to an event when that event is an input to its own responsibility. The problem is making the generic Notification Service decide which business events deserve notification.
The alternative is for the domain service that understands the business situation to decide that a notification is required and send a command to the Notification Service:
Order Service
│
│ SendNotification
▼
Notification Service
For example:
Order Service:
OrderFulfilled
↓
Customer should be informed
↓
SendNotification(...)
The Notification Service does not need to know what an order is. It receives a SendNotification command and performs the notification.
The service requesting the notification can configure the notification itself — including the template/content and the channels that should be used — without requiring those details to be embedded in the command contract. The Notification Service can then apply concerns that belong to notification delivery, such as consumer preferences, when determining how the notification is ultimately delivered.
Conceptually:
Order Service
│
│ SendNotification
▼
Notification Service
│
├── configured notification/template
├── consumer preferences
├── channel selection
└── delivery
The Order Service is the logical boundary that understands Order semantics. If the Order domain says that a customer must be informed when an order is fulfilled, producing a notification command is simply part of that domain's behavior.
It is not a problem that the resulting action crosses a service boundary.
The separation is:
Order Service
knows:
when a notification is required
Notification Service
knows:
how to deliver the notification
The SendNotification message is simply the contract between the two.
Saying that the Order Service is "coupled to the Notification Service" because it knows the SendNotification contract is not, by itself, a valid criticism.
That coupling is intentional and contractual.
The distinction that matters is:
Contract coupling
Order Service ── SendNotification ──► Notification Service
Normal and expected.
versus:
Implementation coupling
Order Service knows Notification Service's internals
Bad.
The fact that one service sends a command defined by another service is not itself an architectural smell.
A different design is to introduce a mapper context between events and the generic Notification capability.
Conceptually:
Events
│
▼
┌──────────────────┐
│ Mapper Context │
│ │
│ domain-specific │
│ mapping │
└────────┬─────────┘
│
│ notification command
▼
Notification Service
The mapper can translate something like:
OrderFulfilled
↓
SendNotification(OrderFulfilledNotification, ...)
The point is not that the mapper somehow becomes the owner of the Order domain. The mapping represents the relationship between the Order domain and the generic Notification capability.
The benefit is that the domain-to-notification integration mapping can be isolated from both the generic Notification Service and the core implementation of the domain service.
Nick Tune describes two relevant forms of mapper contexts: a supercontext/domain mapper approach and a gateway domain mapper context.
The mapper can sit between the domain-specific context and the generic context as part of a broader supercontext:
┌────────────────────────────────────┐
│ Supercontext │
│ │
│ Order Context │
│ │ │
│ ▼ │
│ Mapper Context │
│ │ │
│ ▼ │
│ Notification Context │
│ │
└────────────────────────────────────┘
The important boundary is conceptual. The mapper does not necessarily need to be a separately deployed service.
The mapper can instead sit at the gateway to an external or commodity notification capability:
Internal Domain Events
│
▼
┌─────────────────────────┐
│ Gateway Domain Mapper │
│ Context │
│ │
│ domain-specific mapping │
└───────────┬─────────────┘
│
▼
External Notification
Capability
This is particularly useful when the notification capability becomes an external service or SaaS product. The gateway mapper isolates the internal domain's representation from the external provider's contract.
The mapper does not have to exist from the beginning.
A pragmatic progression is:
Order Service
│
│ SendNotification
▼
Notification Service
Each domain service decides when a notification is required and sends the appropriate command.
The Notification Service remains generic and focuses on notification delivery.
This is often the sensible place to start because it is straightforward and keeps each business decision in the context that understands it.
As the number of mappings and integrations grows, the event-to-notification mapping can be extracted:
Order Events
│
▼
Mapper Context
│
▼
Notification Commands
│
▼
Notification Service
This is an evolution of the integration boundary, not a correction of some supposed flaw in the direct-command design.
If the notification capability becomes an external or commodity service:
Domain Contexts
│
▼
Gateway Domain Mapper Context
│
▼
External Notification Provider
The Gateway Domain Mapper Context shields the internal domain model from the external contract.
The responsibilities can be kept simple:
| Responsibility | Where it belongs |
|---|---|
| Understand Order semantics | Order Context |
| Decide that an Order situation requires a notification | Order Context |
| Understand Payment semantics | Payment/Billing Context |
| Decide that a Payment situation requires a notification | Payment/Billing Context |
| Define the notification contract | Notification integration boundary |
| Deliver email/SMS/push notifications | Notification Service |
| Manage notification providers and delivery concerns | Notification Service |
| Translate domain events into notification commands, when extracted | Mapper Context |
| Translate notification commands for an external notification capability, when extracted | Gateway Domain Mapper Context |
The important distinction is that the domain decision remains with the domain that has the relevant business knowledge. Extracting a mapper changes where the integration mapping is implemented; it does not mean the generic Notification Service should acquire that domain knowledge.
The useful principle is not:
"Services should not know about other services."
Nor is it:
"Avoid coupling at all costs."
Those are overly simplistic rules.
A better principle is:
The context that has the business knowledge should make the corresponding business decision.
Therefore:
Order Context
"This business situation requires a notification."
│
▼
SendNotification
│
▼
Notification Service
"I deliver it."
The push model is therefore a valid and often pragmatic starting point.
The mapper-context model is an alternative that can be introduced later when separating the domain-specific mapping becomes valuable.