Primary reference: https://cap.cloud.sap/docs/java/ Event handlers: https://cap.cloud.sap/docs/java/event-handlers/ Application services: https://cap.cloud.sap/docs/java/cqn-services/application-services Spring Boot integration: https://cap.cloud.sap/docs/java/spring-boot-integration
CAP Java uses Spring Boot as its application framework. The recommended build is cds-framework-spring-boot. We highly recommend configuring cds-framework-spring-boot as the application framework — it provides a lot of integration with CAP out of the box, as well as enhanced features such as dependency injection and auto configuration.
Version requirements (CAP Java 5.x / CDS 10)
| Component | Minimum | Recommended |
|---|---|---|
| JDK | 21 (JDK 17 dropped in CAP Java 5) | 25 (LTS) |
| Spring Boot | 4.0 | 4.1 (with Spring Security 7.0) |
| Maven | 3.9.14+ | Latest stable |
Maven — pom.xml essentials
<parent>
<groupId>com.sap.cds</groupId>
<artifactId>cds-services-bom</artifactId>
<version>use.latest.version</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<!-- CAP Java framework — Spring Boot integration -->
<dependency>
<groupId>com.sap.cds</groupId>
<artifactId>cds-framework-spring-boot</artifactId>
<scope>runtime</scope>
</dependency>
<!-- OData V4 adapter -->
<dependency>
<groupId>com.sap.cds</groupId>
<artifactId>cds-adapter-odata-v4</artifactId>
<scope>runtime</scope>
</dependency>
<!-- HANA Cloud database -->
<dependency>
<groupId>com.sap.cds</groupId>
<artifactId>cds-feature-hana</artifactId>
<scope>runtime</scope>
</dependency>
<!-- H2 for local development -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>com.sap.cds</groupId>
<artifactId>cds-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
Event Handler class structure
The @Component annotation instructs Spring Boot to create a bean instance from the class. The EventHandler marker interface is required for CAP to identify the class as an event handler class among all beans and scan it for event handler methods. The optional @ServiceName annotation specifies the default service which event handlers are registered on.
package com.example.handlers;
import org.springframework.stereotype.Component;
import com.sap.cds.services.handler.EventHandler;
import com.sap.cds.services.handler.annotations.ServiceName;
import com.sap.cds.services.handler.annotations.Before;
import com.sap.cds.services.handler.annotations.On;
import com.sap.cds.services.handler.annotations.After;
import com.sap.cds.services.cds.CqnService;
import com.sap.cds.services.cds.CdsCreateEventContext;
import com.sap.cds.services.cds.CdsReadEventContext;
@Component
@ServiceName("OrderService") // default service for all methods
public class OrderServiceHandler implements EventHandler {
@Autowired
private PersistenceService db;
// Before handler — validation, enrichment before persistence
@Before(event = CqnService.EVENT_CREATE, entity = Orders_.CDS_NAME)
public void beforeCreateOrder(List<Orders> orders) {
for (Orders order : orders) {
if (order.getTotalAmount() == null || order.getTotalAmount().signum() <= 0) {
throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Total amount must be positive");
}
}
}
// On handler — full control, replaces default behavior
@On(event = CqnService.EVENT_READ, entity = Orders_.CDS_NAME)
public void onReadOrders(CdsReadEventContext context) {
// Run custom query
CqnSelect query = context.getCqn();
Result result = db.run(query);
context.setResult(result);
context.setCompleted();
}
// After handler — post-processing, side effects
@After(event = CqnService.EVENT_CREATE, entity = Orders_.CDS_NAME)
public void afterCreateOrder(List<Orders> orders) {
// Send notification, update cache, etc.
orders.forEach(order -> log.info("Order created: {}", order.getId()));
}
}
CQL queries in Java
// SELECT
CqnSelect select = Select.from(Orders_.class)
.columns(o -> o.id(), o -> o.status(), o -> o.totalAmount())
.where(o -> o.status().eq("Open"));
Result result = db.run(select);
List<Orders> orders = result.listOf(Orders.class);
// INSERT
Orders newOrder = Orders.create();
newOrder.setId(UUID.randomUUID().toString());
newOrder.setStatus("Open");
CqnInsert insert = Insert.into(Orders_.class).entry(newOrder);
db.run(insert);
// UPDATE
CqnUpdate update = Update.entity(Orders_.class)
.data("status", "Approved")
.where(o -> o.id().eq(orderId));
db.run(update);
// DELETE
CqnDelete delete = Delete.from(Orders_.class)
.where(o -> o.id().eq(orderId));
db.run(delete);
Custom action handler
// CDS definition: action submitOrder(orderID: UUID) returns String;
@On(event = "submitOrder", entity = Orders_.CDS_NAME)
public void onSubmitOrder(CdsActionEventContext context) {
String orderId = context.get("orderID");
// Update status
db.run(Update.entity(Orders_.class)
.data("status", "Submitted")
.where(o -> o.id().eq(orderId)));
context.setResult("Order " + orderId + " submitted successfully");
context.setCompleted();
}
Injecting services
@Component
@ServiceName("OrderService")
public class OrderServiceHandler implements EventHandler {
@Autowired
private PersistenceService db; // database access
@Autowired
@Qualifier("ExternalService")
private RemoteService externalService; // remote OData service
@Autowired
private ApplicationLifecycleService als; // app lifecycle events
}
Error handling
import com.sap.cds.services.ServiceException;
import com.sap.cds.services.ErrorStatuses;
// Reject with HTTP status
throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Invalid input: {}", fieldValue);
throw new ServiceException(ErrorStatuses.NOT_FOUND, "Order not found: {}", orderId);
throw new ServiceException(ErrorStatuses.FORBIDDEN, "Not authorized");
// With message key (i18n)
throw new ServiceException(ErrorStatuses.CONFLICT, MessageKeys.DUPLICATE_ORDER);
application.yaml — profiles
spring:
config:
activate:
on-profile: default
datasource:
url: jdbc:h2:mem:testdb # H2 for local dev
---
spring:
config:
activate:
on-profile: cloud
cds:
datasource:
auto-config:
enabled: true # auto-configure HANA from VCAP_SERVICES
Common mistakes to avoid
-
❌ Implementing
EventHandlerwithout@Component— Spring won't create the bean -
✅ Always annotate with both
@Componentandimplements EventHandler -
❌ Using
@Onwhen you only need validation — replaces default persistence behavior -
✅ Use
@Beforefor validation/enrichment,@Afterfor side effects,@Ononly when replacing default behavior -
❌ Running CQL queries without injecting
PersistenceService— no database access -
✅ Always
@Autowired PersistenceService dbfor database operations -
❌ Not calling
context.setCompleted()in an@Onhandler — framework may run default behavior anyway -
✅ Call
context.setCompleted()at the end of every@Onhandler -
❌ Using
com.sap.cdsas your package GroupId for custom plugins -
✅ This namespace is reserved for SAP — use your own groupId
-
❌ Hardcoding entity names as strings:
entity = "OrderService.Orders" -
✅ Use generated constants:
entity = Orders_.CDS_NAME— type-safe, refactor-proof
Migration from CAP Java 4 to 5 — OpenRewrite
CAP Java 5 provides OpenRewrite recipes to automate the bulk of required code changes:
mvn org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=com.sap.cds:cds-services-recipes:5.0.0 \
-Drewrite.activeRecipes=com.sap.cds.services.migrations.Cap_5.0
Built-in OData processing (CAP Java 5+)
CAP Java 5 replaced Apache Olingo (which was retired in 2025) with built-in OData processing. The internal modules repackaged/odata-v4-lib and repackaged/odata-v2-lib have been removed. If your code directly referenced Olingo classes, rewrite it using CAP Java native APIs.
Harmonized Search (CAP Java 5+)
Search now uses the SAP HANA search syntax by default (harmonized with CAP Node.js and RAP):
| Operator | SAP HANA syntax | OData syntax |
|---|---|---|
| AND | space | AND |
| OR | OR |
OR |
| NOT | - |
NOT |
Fuzzy search is enabled by default on SAP HANA. To switch to OData search syntax:
# application.yaml
cds:
sql:
search:
syntax: ODATA
Faster Typed Data Access — generateClasses
Generate implementation classes (instead of dynamic proxies) for accessor interfaces — up to 10× faster typed access:
<!-- srv/pom.xml — CDS Maven Plugin generate goal -->
<plugin>
<groupId>com.sap.cds</groupId>
<artifactId>cds-maven-plugin</artifactId>
<executions>
<execution>
<id>cds.generate</id>
<goals><goal>generate</goal></goals>
<configuration>
<generateClasses>true</generateClasses>
</configuration>
</execution>
</executions>
</plugin>