Skip to content
Open
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 105 additions & 14 deletions node.js/messaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,102 @@ status: released
<!--- % include links-for-node.md %} -->
<!--- % include _chapters toc="2,3" %} -->

## Overview

In SAP Cloud Application Programming Model (CAP), messaging enables decoupled communication between services using events.
CAP distinguishes between the logical and technical messaging layers, separating business concerns from technical infrastructure and enabling both flexibility and scalability.

The **logical layer** consists of three primary components:

**Modeled Events**: Events are defined in CDS models with typed schemas, providing compile-time validation and IDE support. These events represent business occurrences like `'orderProcessed'`, or `'stockUpdated'`.

**Event Topics**: Topics organize events into logical channels, allowing for structured event categorization and routing. Topics can be explicitly defined or derived from service and event names.

**CAP Services**: Services act as event producers and consumers, using simple APIs like `srv.emit('reviewed', data)` and `srv.on('orderProcessed', handler)`. Services communicate using friendly event names without needing to know the underlying infrastructure details.

The **technical layer** handles the actual message transport and delivery:

**CAP Messaging Service**: Acts as the translation layer between logical events and technical infrastructure. It converts service-level event names to fully qualified names (e.g., `'OrderSrv.reviewed'`), manages topic resolution, message serialization, and routing logic.

**Message Brokers**: Form the core of the technical infrastructure, handling message persistence, delivery guarantees, and cross-service communication. Examples include SAP Event Mesh, Apache Kafka, or Redis Streams.

The message flow follows a clear path through both layers:

**Outbound Flow (Publisher)**: A service calls `srv.emit('reviewed', data)` → CAP Messaging Service resolves the event name to a fully qualified topic (e.g., `OrderSrv.reviewed`) → Message is serialized and sent to the Event Broker → Broker stores and distributes the message to all subscribers.

**Inbound Flow (Subscriber)**: Event Broker delivers message from subscribed topic → CAP Messaging Service receives the message → Service name and event name are resolved from the topic → Message is routed to the appropriate service handler via `srv.on('reviewed', handler)`.

**Alternatively** custom handlers can bypass the service layer and work directly with the messaging service:

```javascript
// Direct access to messaging service
const messaging = await cds.connect.to('messaging');

// Send messages directly with full topic control
await messaging.emit('custom.topic', data);

// Receive messages directly from any topic
messaging.on('external.system.events', handler);
```

### Topic Resolution

Topic resolution is an important part of the integration between logical and technical messaging layers.

Every technical message requires a topic. If a CDS event is declared without an @topic annotation, CAP uses the event’s fully qualified name (FQN) as the topic (e.g., OrderSrv.reviewed). If an event is declared with an @topic: 'foo.bar' annotation, the specified string is used as the topic (e.g., foo.bar).

### Emitting and Receiving Events

#### Scenario 1: No @topic Annotation

When a CDS event is declared without an @topic annotation in a service (for example, an event called 'reviewed' in the OrderSrv service), the messaging behavior follows this pattern:

**Emitting Events:**
• Using the logical service API, you emit the event with just the event name ('reviewed')
• Using the technical messaging API, you emit using the fully qualified name ('OrderSrv.reviewed')

**Broker Handling:**
• The message broker receives and routes the message using the topic 'OrderSrv.reviewed'

**Receiving Events:**
• Using the logical service API, you listen for the event with just the event name ('reviewed')
• Using the technical messaging API, you listen using the fully qualified name ('OrderSrv.reviewed')

#### Scenario 2: With @topic Annotation

When a CDS event is declared with a custom @topic annotation (for example, an event called 'reviewed' with @topic: 'foo.bar' in the OrderSrv service), the messaging behavior follows this pattern:

**Emitting Events:**
• Using the logical service API, you still emit the event with the event name ('reviewed')
• Using the technical messaging API, you emit using the custom topic name ('foo.bar')

**Broker Handling:**
• The message broker receives and routes the message using the custom topic 'foo.bar'

**Receiving Events:**
• Using the logical service API, you listen for the event with the event name ('reviewed')
• Using the technical messaging API, you listen using the custom topic name ('foo.bar')

### Summary Table


| CDS Event Declaration | Emitting via `srv.emit` | Emitting via `messaging.emit` | Broker Topic | Receiving via `srv.on` | Receiving via `messaging.on` |
|------------------------------|-------------------------|-------------------------------|----------------------|------------------------|------------------------------|
| No `@topic` | `'reviewed'` | `'OrderSrv.reviewed'` | `OrderSrv.reviewed` | `'reviewed'` | `'OrderSrv.reviewed'` |
| With `@topic: 'foo.bar'` | `'reviewed'` | `'foo.bar'` | `foo.bar` | `'reviewed'` | `'foo.bar'` |


### Key Points

- Logical service API (srv.emit, srv.on) uses the event name as declared in CDS.
- Technical messaging API (messaging.emit, messaging.on) uses the resolved topic name.
- If no @topic is specified, the topic defaults to the event’s fully qualified name.
- If @topic is specified, it overrides the default topic name.

## cds.**MessagingService** <i> class </i>

Class `cds.MessagingService` and subclasses thereof are technical services representing asynchronous messaging channels.
They can be used directly/low-level, or behind the scenes on higher-level service-to-service eventing.
Class `cds.MessagingService` and subclasses thereof are technical services representing asynchronous messaging channels.
They can be used directly/low-level, or behind the scenes on higher-level service-to-service eventing.

### class cds.**MessagingService** <i> extends cds.Service </i>

Expand Down Expand Up @@ -55,7 +146,7 @@ In _srv/external/external.cds_:
service ExternalService {
event ExternalEvent {
ID: UUID;
name: String;
rating: Decimal;
}
}
```
Expand All @@ -66,7 +157,7 @@ In _srv/own.cds_:
service OwnService {
event OwnEvent {
ID: UUID;
name: String;
rating: Decimal;
}
}
```
Expand Down Expand Up @@ -94,7 +185,7 @@ Example:
```cds
service OwnService {
@topic: 'my.custom.topic'
event OwnEvent { ID: UUID; name: String; }
event OwnEvent { ID: UUID; rating: Decimal; }
}
```

Expand All @@ -109,19 +200,19 @@ Example:
const messaging = await cds.connect.to('messaging')

this.after(['CREATE', 'UPDATE', 'DELETE'], 'Reviews', async (_, req) => {
const { subject } = req.data
const { ID } = req.data
const { rating } = await cds.run(
SELECT.one(['round(avg(rating),2) as rating'])
.from(Reviews)
.where({ subject }))
.where({ ID }))

// send to a topic
await messaging.emit('cap/msg/system/review/reviewed',
{ subject, rating })
await messaging.emit('cap/msg/system/my/custom/topic',
{ ID, rating })

// alternative if you want to send custom headers
await messaging.emit({ event: 'cap/msg/system/review/reviewed',
data: { subject, rating },
await messaging.emit({ event: 'cap/msg/system/my/custom/topic',
data: { ID, rating },
headers: { 'X-Correlation-ID': req.headers['X-Correlation-ID'] }})
})
```
Expand All @@ -141,9 +232,9 @@ Example:
const messaging = await cds.connect.to('messaging')

// listen to a topic
messaging.on('cap/msg/system/review/reviewed', msg => {
const { subject, rating } = msg.data
return cds.run(UPDATE(Books, subject).with({ rating }))
messaging.on('cap/msg/system/my/custom/topic', msg => {
const { ID, rating } = msg.data
return cds.run(UPDATE(Books, ID).with({ rating }))
})
```

Expand Down