Skip to content
Draft
Changes from all 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
In SAP Cloud Application Programming Model (CAP), messaging enables decoupled communication between services using events.
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
CAP distinguishes between the logical and technical messaging layers, separating business concerns from technical infrastructure and enabling both flexibility and scalability.
CAP distinguishes between the logical and technical messaging layers, separating business concerns from technical infrastructure.

Don't add AI slop ;)


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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"structured event categorization"? What is that?


**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.
Copy link
Contributor

@David-Kunz David-Kunz Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**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.
**CAP Services**: Services act as event producers or consumers, using simple APIs like `srv.emit('reviewed', data)` or `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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems weird. Maybe "Logical events are delegated to the messaging service, the corresponding event name on the technical service is either the fully qualified event name or the value of the @topic annotation if given."


**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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe mention that A service is an own service (not an external one).


**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)`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also mention that the existence of a srv.on event handler for moedelled event will cause the broker to listen to those (e.g. subscriptions for Event Mesh).


**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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds a bit misleading, not any topic, but a particular 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')
Comment on lines +65 to +91
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a lot of explanation for the topic mapping -> should be condensed


### 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

headers can also be the 3rd argument, i.e. messaging.emit('topic', data, 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