Emit and subscribe to in-process events using this.emit(). Decouple handler logic with event-driven communication.
cds.emit() and this.emit()Event-driven architecture decouples services: an order service doesn't need to know about the notification service, the audit service, or the loyalty service. CDS events let you build this within a single CAP deployment, cleanly and without message brokers for in-process cases.
// srv/orders-service.cds
service OrdersService {
entity Orders as projection on my.Orders;
event OrderSubmitted {
ID : UUID;
amount : Decimal;
buyer : String;
}
event OrderCancelled {
ID : UUID;
reason : String;
}
}
Events are declared in the service that emits them. Other services subscribe to them.
// srv/orders-service.js
this.on('submitOrder', async (req) => {
const { orderId } = req.data
await UPDATE('Orders').set({ status: 'SUBMITTED' }).where({ ID: orderId })
const order = await SELECT.one.from('Orders').where({ ID: orderId })
// Emit the event — all in-process subscribers receive it
await this.emit('OrderSubmitted', {
ID: order.ID,
amount: order.total,
buyer: req.user.id
})
return order
})
this.on('OrderSubmitted', async (msg) => {
console.log('Order submitted:', msg.data.ID, 'by', msg.data.buyer)
// msg.data carries the event payload
// This runs in the same transaction if emitted from within a handler
})
// srv/notification-service.js
const cds = require('@sap/cds')
module.exports = class NotificationService extends cds.ApplicationService {
async init () {
// Subscribe to events from another service
const orders = await cds.connect.to('OrdersService')
orders.on('OrderSubmitted', async (msg) => {
await this.sendEmail({
to: msg.data.buyer,
subject: `Order ${msg.data.ID} confirmed`,
body: `Total: €${msg.data.amount}`
})
})
await super.init()
}
}
this.on('OrderSubmitted', async (msg) => {
const { ID, amount, buyer } = msg.data // typed payload from CDS event def
const tenant = msg.tenant // for multitenant apps
const headers = msg.headers // optional headers
})
By default, in-process events are synchronous within the request transaction — the event handler runs before the outer transaction commits.
// Both the UPDATE and the audit log are in the same DB transaction
await UPDATE('Orders').set({ status: 'SUBMITTED' }).where({ ID })
await this.emit('OrderSubmitted', { ID, amount }) // audit runs here, same tx
For fire-and-forget (run after commit):
cds.context = cds.context || {}
cds.spawn({ tenant: req.tenant }, async () => {
await this.emit('OrderSubmitted', { ID, amount })
})
| In-process event | Direct service call |
|---|---|
| Emitter doesn't know subscribers | Caller explicitly invokes callee |
| Loose coupling | Tight coupling |
| Multiple subscribers possible | One callee |
| Order of execution not guaranteed | Sequential |
| Use for: notifications, audit, side-effects | Use for: business logic that must complete |
// After any business operation, emit a domain event
this.after('CREATE', 'Orders', (order, req) => this.emit('OrderCreated', { ...order, actor: req.user.id }))
this.after('UPDATE', 'Orders', (order, req) => this.emit('OrderUpdated', { ...order, actor: req.user.id }))
this.on('cancelOrder', 'Orders', async (req) => {
// ... cancel logic ...
await this.emit('OrderCancelled', { ID, reason: req.data.reason })
})
event BookPurchased { bookId: UUID; quantity: Integer; buyer: String; } in your servicepurchaseBook action after updating stockPurchaseLog entityAnalyticsService that counts purchasesthis.emit() fires in-process; all in-process subscribers receive itcds.connect.to('ServiceName').on('EventName', ...)cds.spawn()