Skip to main content Link Menu Expand (external link) Document Search Copy Copied

CAP Outbox

The event-queue can be used to replace the CAP outbox solution to achieve a unified and streamlined architecture for asynchronous processing. If this feature is activated, the event-queue replaces the outbox implementation of CAP with its own implementation during the bootstrap process. This allows leveraging the features of the event-queue, such as transaction modes, load balancing, and others, with outboxed CDS services.

How to enable the event-queue as outbox mechanism for CAP

The initialization parameter useAsCAPOutbox enables the event-queue to act as a CAP outbox. To set this parameter, refer to the setup part of the documentation. This is the only configuration needed to enable the event-queue as a CAP outbox.

{
  "cds": {
    "eventQueue": { "useAsCAPOutbox": true }
  }
}

How to Configure an Outboxed Service

Services can be outboxed without any additional configuration. In this scenario, the service is outboxed using the default parameters of the CAP outbox and the event-queue. Currently, the CAP outbox implementation supports the following parameters, which are mapped to the corresponding configuration parameters of the event-queue:

CAP outbox event-queue CAP default
chunkSize selectMaxChunkSize 100
maxAttempts retryAttempts 20
parallel parallelEventProcessing yes (mapped to 5)
- useEventQueueUser false

The parallel parameter is treated specially. The CAP outbox supports true or false as values for this parameter. Since the event-queue allows specifying concurrency with a number, parallel=true is mapped to parallelEventProcessing=5, and parallel=false is mapped to parallelEventProcessing=1. For full flexibility, the configuration prioritizes the parallelEventProcessing parameter over parallel.

The useEventQueueUser parameter can be set to true or false. When set to true, the user defined in the general configuration will be used as the cds context user (context.user.id). This influences actions such as updating managed database fields like modifiedBy. The default value for this parameter is false.

All supported parameters available for EventQueueProcessors are also available for CAP outboxed services. This means that you can use the same configuration settings and options that you would use with EventQueueProcessors when configuring CAP outboxed services, ensuring consistent behavior and flexibility across both use cases.

Parameters are managed via the cds.require section, not through the config yml file as with other events. For details on maintaining the cds.requires section, refer to the CAP documentation. Below is an example of using the event-queue to outbox the @cap-js/audit-logging service:

{
  "cds": {
    "requires": {
      "audit-log": {
        "outbox": {
          "kind": "persistent-outbox",
          "transactionMode": "alwaysRollback",
          "maxAttempts": 5,
          "checkForNextChunk": false,
          "parallelEventProcessing": 5
        }
      }
    }
  }
}

The parameters in the outbox section of a service are passed as configuration to the event-queue. The persistent-outbox kind allows the event-queue to persist events instead of executing them in memory, mirroring the behavior of the CAP outbox. The parameters transactionMode, checkForNextChunk, and parallelEventProcessing are exclusive to the event-queue.

Periodic Actions/Events in Outboxed Services

The event-queue supports to periodically schedule actions of a CAP service based on a cron expression or defined interval. Every event/action in CAP Service can have a different periodicity. The periodicity is defined in the outbox section of the service configuration. In the example below the syncTasks action is scheduled every 15 minutes and the masterDataSync action is scheduled every day at 3:00 AM.

{
  "my-service": {
    "outbox": {
      "kind": "persistent-outbox",
      "events": {
        "syncTasks": { "cron": "*/15 * * * *" },
        "masterDataSync": { "cron": "0 3 * * *" }
      }
    }
  }
}

Configure certain actions differently in the same CAP Service

It is possible to configure different actions in the same CAP service differently. In the example below, all actions in the service are configured with a maxAttempts of 1, except for the importantTask action, which is configured with a maxAttempts of 20. Based on this logic all configuration parameters can be set differently but having a reasonable default configuration for the service.

{
  "my-service": {
    "outbox": {
      "kind": "persistent-outbox",
      "maxAttempts": 1,
      "events": { "importantTask": { "maxAttempts": 20 } }
    }
  }
}

Example of a Custom Outboxed Service

Internal Service with cds.Service

The implementation below demonstrates a basic cds.Service that can be outboxed. If you want to configure outboxing via cds.env.requires, the service needs to inherit from cds.Service.

class TaskService extends cds.Service {
  async init() {
    await super.init();
    this.on("process", async function (req) {
      // add your code here
    });
  }
}

Outboxing can be enabled via configuration using cds.env.requires, for example, through package.json.

{
  "cds": {
    "requires": {
      "task-service": {
        "impl": "./srv/PATH_SERVICE/taskService.js",
        "outbox": {
          "kind": "persistent-outbox",
          "transactionMode": "alwaysRollback",
          "maxAttempts": 5,
          "parallelEventProcessing": 5
        }
      }
    }
  }
}

Application Service with cds.ApplicationService

In contrast, cds.ApplicationService, which is served based on protocols like odata-v4, cannot be outboxed via configuration (cds.env.requires). Nevertheless, outboxing can be performed manually as shown in the example below:

const service = await cds.connect.to("task-service");
const outboxedService = cds.outboxed(service, {
  kind: "persitent-outbox",
  transactionMode: "alwaysRollback",
});
await outboxedService.send("process", {
  ID: 1,
  comment: "done",
});

How to cluster multiple outbox events

This functionality allows multiple service calls to be processed in a single batch, reducing redundant executions. Instead of invoking a service action separately for each event, clustering groups similar events together and processes them in one call.

This approach is particularly useful when multiple events trigger the same action. For example, if an action is responsible for sending emails, clustering can combine all relevant events into a single email. See the example below for implementation in a CAP service:

this.on("clusterQueueEntries", (req) => {
  return req.eventQueue.clusterByDataProperty("to", (clusterKey, clusterEntries) => {
    // clusterKey is the value of the property "req.data.to"
    // clusterEntries is an array of all entries with the same "req.data.to" value
    return { recipients: clusterKey, mails: clusterEntries };
  });
});

If different actions require different clustering logic, you can define action-specific clustering. The following example clusters only the sendMail action:

this.on("clusterQueueEntries.sendMail", (req) => {
  return req.eventQueue.clusterByDataProperty("to", (clusterKey, clusterEntries) => {
    // clusterKey is the value of the property "req.data.to"
    // clusterEntries is an array of all entries with the same "req.data.to" value
    return { recipients: clusterKey, mails: clusterEntries };
  });
});

The event-queue provides three basic clustering helper functions:

  • clusterByDataProperty: Clusters events based on a specific req.data property from the original action call.
  • clusterByPayloadProperty: Clusters events based on properties from the raw action call payload, such as req.event, contextUser, headers, and more. This can be used, for example, to group all calls of the same action into a single batch. To achieve this, pass "event" as the first parameter to this function.
  • clusterByEventProperty: Clusters events based on raw event data, which corresponds to the event database entry. This allows grouping by fields such as referenceEntityKey, attempts, status, and more.

Event-queue follows a priority order when applying clustering:

  1. It first checks for an action-specific implementation (e.g., clusterQueueEntries.sendMail).
  2. If none is found, it falls back to the general implementation (clusterQueueEntries).
  3. If neither is implemented, no clustering is performed.

Register hook for exceeded events retries

Event-queue provides a hook that allows you to register a function to be called when an event exceeds the maximum number of retry attempts. This hook enables you to implement custom logic within a managed transaction.

If the hook is triggered, the failed service call gets up to three additional retry attempts. Regardless of the outcome, the event will ultimately be marked as Exceeded. The exceeded hook can be registered for the entire service or for specific actions, following the same approach as clusterQueueEntries. See the example below a generic exceeded hook and an action-specific exceeded hook:

this.on("hookForExceededEvents", (req) => {
  // provides a manage transaction
});

this.on("hookForExceededEvents.sendMail", (req) => {
  // provides a manage transaction
});

How to Delay Outboxed Service Calls

The event queue has a feature that enables the publication of delayed events. This feature is also applicable to CAP outboxed services.

To implement this feature, include the x-eventqueue-startAfter header attribute during the send or emit process.

const outboxedService = await cds.connect.to("task-service");
await outboxedService.send(
  "process",
  {
    ID: 1,
    comment: "done",
  },
  // delay the processing 4 minutes
  { "x-eventqueue-startAfter": new Date(Date.now() + 4 * 60 * 1000).toISOString() }
);

Additional parameters Outboxed Service Calls

Similar to delaying published events, it is also possible to provide other parameters when publishing events. All event publication properties can be found here.

Error Handling in a Custom Outboxed Service

If the custom service is invoked via service.send(), errors can be raised with req.reject() and req.error(). The reject and error functions cannot be used if the service call is made via service.emit(). Refer to the example below for an implementation reference.

class TaskService extends cds.Service {
  async init() {
    await super.init();
    this.on("rejectEvent", (req) => {
      req.reject(404, "error occured");
    });

    this.on("errorEvent", (req) => {
      req.error(404, "error occured");
    });
  }
}

Errors raised in a custom outboxed service are thrown and will be logged from the event queue. The event entry will be marked as an error and will be retried based on the event configuration.

Event-Queue properties

The event queue properties that are available for the native event queue processor (refer to this documentation) are also accessible for outboxed services utilizing the event queue. These properties can be accessed via the cds request. The following properties are available:

  • processor: instance of event-queue processor
  • key
  • queueEntries
  • payload
class TaskService extends cds.Service {
  async init() {
    await super.init();
    this.on("send", (req) => {
      const { processor, queueEntries, payload, key } = req.eventQueue;
    });
  }
}

How to return a custom status?

It’s possible to return a custom status for an event. The allowed status values are explained here.

this.on("returnPlainStatus", (req) => {
  return EventProcessingStatus.Done;
});