Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save bymyslf/ef9675fe6b5f0089fe6a826777d3a3fb to your computer and use it in GitHub Desktop.

Select an option

Save bymyslf/ef9675fe6b5f0089fe6a826777d3a3fb to your computer and use it in GitHub Desktop.
Notification Service: Events, Commands, and Mapper Contexts

Notification Service: Events, Commands, and Mapper Contexts

The design problem

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.


1. Notification Service consumes events

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 OrderCreated matters;
  • why OrderFulfilled does 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.


2. Domain services send notification commands

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

This is not a design problem

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.

Messages and APIs are contracts

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.


3. The Mapper Cntext alternative

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.


4. Two mapper variants

Nick Tune describes two relevant forms of mapper contexts: a supercontext/domain mapper approach and a gateway domain mapper context.

Supercontext / Domain Mapper

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.

Gateway Domain Mapper Context

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.


5. Evolutionary approach

The mapper does not have to exist from the beginning.

A pragmatic progression is:

Stage 1: Push Model

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.

Stage 2: Mapper Context

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.

Stage 3: Gateway Domain Mapper Context

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.


6. What belongs where

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.


7. The architectural principle

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.


References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment