Building Plugins
A collection of different mechanisms that can be used to build plugins for CAP Java.
Especially, when working with larger projects that may consist of many individual CAP Java applications or when building platform services that need to be integrated with CAP applications there's the requirement to extend CAP Java with custom, yet reusable code.
In the following sections, the different extension points and mechanisms are explained.
General Considerations
Java Version
When building CAP Java plugin modules, you need to keep in mind that the generated Java byte code of the plugin has to be compatible with the Java byte code version of the potential consumers of the plugin. To be on the safe side, we recommend using Java 17 as this is anyways the minimum Java version for CAP Java applications. In case you deviate from this you need to check and align with the potential consumers of the plugin.
Maven GroupId and Java Packages
Of course, it's up to your project / plugin how you call the corresponding Maven GroupId and Java packages. To avoid confusion and also to make responsibilities clear com.sap.cds for GroupId and Java package names are reserved for components maintained by the CAP Java team and must not be used for external plugins. This rule also includes substructures to com.sap.cds like com.sap.cds.foo.plugin.
Note on the reference plugins
The example plugins referenced later in this guide (in the Plugin Conventions section) are hosted in the cap-java GitHub organization. Some of them are maintained and published by the CAP Java team under the com.sap.cds namespace — which is why they use that groupId. Others are contributions from SAP teams that should have used their own groupId but didn't. In either case they are cited here for their layout, registration, and testing patterns only — not as authorities on Maven groupId choice. As an external plugin author, use your own reverse-domain groupId (for example com.example.mycorp).
Share CDS Models via Maven Artifacts
CDS models, CSV import data and i18n files can be shared through Maven dependencies. In addition they can also be shared through npm packages. This means you can provide CDS models, CSV files, i18n files, and Java code (for example, event handlers) in a single Maven dependency.
Create the CDS Model in a New Maven Artifact
Simply create a plain Maven Java project and place your CDS models in the src/main/resources/cds folder of the reuse package under a unique module directory (for example, leveraging group ID and artifact ID): src/main/resources/cds/com.sap.capire/bookshop/. With com.sap.capire being the group ID and bookshop being the artifact ID.
You can simplify the creation of such a plain Maven Java project by calling the following Maven archetype command:
mvn archetype:generate -DgroupId=com.sap.capire -DartifactId=bookshop -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=falseAfter the creation you'll need to maintain the plugin versions as well as the desired Java language version (we recommend version 17).
Only plain Maven Java projects
Please make sure that your plugin / reuse model project is neither created as a CAP Java project nor as a plain Spring Boot project.
Reference the New CDS Model in an Existing CAP Java Project
Projects wanting to import the content simply add a Maven dependency to the reuse package to their srv/pom.xml in the <dependencies> section.
<dependency>
<groupId>com.sap.capire</groupId>
<artifactId>bookshop</artifactId>
<version>1.0.0</version>
</dependency>Additionally, the resolve goal from the CDS Maven Plugin needs to be added, to extract the models into the target/cds/ folder of the Maven project, in order to make them available to the CDS Compiler.
<plugin>
<groupId>com.sap.cds</groupId>
<artifactId>cds-maven-plugin</artifactId>
<version>${cds.services.version}</version>
<executions>
...
<execution>
<id>cds.resolve</id>
<goals>
<goal>resolve</goal>
</goals>
</execution>
...
</executions>
</plugin>Scope of the Reuse Package
Usually you would declare the dependency to the reuse package in the srv module of your CAP Java project. Since CAP Java 4.4.0 this makes the reuse models available to all CDS files in the CAP project. The models are extracted to the root target/cds folder. In case you want to make the reuse models only available within the Maven module that declared the dependency (for example, srv) set the configuration to of the resolve goal to ${project.build.directory}. In earlier versions of CAP Java reuse models where only available within CDS files placed in the Maven module that declared the dependency by default.
When your Maven build is set up correctly, you can use the reuse models in your CDS files using the standard using directive:
using { CatalogService } from 'com.sap.capire/bookshop';Different resolution rules
The location in the using directive differs from the default CDS model resolution rules. The name does not refer to a local file/package, nor to an NPM package. Instead, it follows to the groupId/artifactId scheme. The name doesn't directly refer to an actual file system location but is looked up in a cds folder in Maven's target folder.
Learn more about providing and using reuse packages.
This technique can be used independently or together with one or more of the techniques described on this page.
Event Handlers for Custom Types and Annotations
In CAP Java, event handlers aren't tightly coupled to the request handling or any other runtime components. Thus, it's easily possible to package event handlers in external libraries (like plugins) in order to provide common but custom functionality to CAP Java applications. You can achieve this by defining custom handlers that react on model characteristics (common types or annotations) or also on entity values, for example, validations.
In most of the cases an event handler plugin for a CAP Java application can be a plain Maven project without further dependencies or special project layout. Since you need to use or implement CAP Java extension points, it's required to define the following dependencies:
<properties>
<cds.services.version>...</cds.services.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.sap.cds</groupId>
<artifactId>cds-services-bom</artifactId>
<version>${cds.services.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.sap.cds</groupId>
<artifactId>cds-services-api</artifactId>
</dependency>
</dependencies>Info
Please note that you as the plugin developer have to choose a version for ${cds.services.version}. As a best practice, we recommend to take the current LTS version (last minor and patch level of the maintenance release of CAP Java for this version.
With this version your plugin will be consumable by the majority of CAP applications. In order to make sure that your plugin runs with both major versions (current and prior) it is recommended to run integration tests with embedding applications configured for each major version.
Inside your plugin module, you can define a custom event handler and a registration hook as plain Java code. Once this module is deployed to a Maven repository it can be added to any CAP Java application as a dependency. The contained event handler code is active automatically once your CAP Java application is started along with the new reuse module.
The heart of the plugin module, the event handler, basically looks like any other CAP Java event handler. Take this one as an example:
@ServiceName(value = "*", type = ApplicationService.class)
public class SampleHandler implements EventHandler {
@After
public void handleSample(CdsReadEventContext context) {
// any custom Java code using the event context and CQL APIs
}
}The shown handler code is registered for any entity type on any ApplicationService. Depending on the use case the target scope could be narrowed to specific entities and/or services. The handler registration applies to the same rules as custom handlers that are directly packaged with a CAP Java application.
Learn more about event handling in our EventHandler documentation
Of course, this handler code looks just the same as any other custom or builtin CAP Java handler. The only difference here is that you need to think a bit more about the provisioning of the handler. When you write a custom handler as part of (in the package of) a CAP Java application, you can annotate the handler's class with @Component. Then Spring Boot's component scan picks up the class during startup of the Application Context.
When you provide your custom handler as part of a reuse library, external to your application, things change a bit. At first, you need to decide whether you want to use Spring Boot's component model and rely on dependency injection or if you want to use one of the CAP Java ServiceLoader based extension points.
The decision between the two is straightforward: In case your handler depends on other Spring components, for example relies on dependency injection, you should use the Spring approach. This applies as soon as you need to access another CAP Service like CqnService, PersistenceService or to a service using it's typed service interface.
If your custom handler is isolated and, for example, only performs a validation based on provided data or a calculation, you can stick with the CAP Java ServiceLoader approach, which is described in the following section.
In practice, the CdsRuntimeConfiguration ServiceLoader approach is the norm across plugins — it works without a Spring context and keeps the plugin framework-independent. Spring AutoConfiguration is the better choice when the plugin needs typed @ConfigurationProperties classes or @ConditionalOnProperty/@ConditionalOnMissingBean wiring. See the Configuration & Enablement section for more on how real-world plugins use these mechanisms.
Load Plugin Code via ServiceLoaders
At runtime, CAP Java uses the ServiceLoader mechanism to load all implementations of the CdsRuntimeConfiguration interface from the application's ClassPath. In order to qualify as a contributor for a given ServiceLoader-enabled interface, we need to place a plain text file, named like the fully qualified name of the interface, in the directory src/main/resources/META-INF/services of our reuse model. This file contains the name of one or more implementing classes. For the earlier implemented CdsRuntimeConfiguration we need to create a file src/main/resources/META-INF/services/com.sap.cds.services.runtime.CdsRuntimeConfiguration with the following content:
com.sap.example.cds.SampleHandlerRuntimeConfigurationWith this code you instrument the CAP Java's ServiceLoader for CdsRuntimeConfiguration to load our new, generic EventHandler for all read events on all entities of all services. For realistic use cases, the handler configuration can be more concise, of course.
So, in order to have a framework independent handler registration the CdsRuntimeConfiguration interface needs to be implemented like this:
package com.sap.example.cds;
import com.sap.cds.services.runtime.CdsRuntimeConfiguration;
import com.sap.cds.services.runtime.CdsRuntimeConfigurer;
public class SampleHandlerRuntimeConfiguration implements CdsRuntimeConfiguration {
@Override
public void eventHandlers(CdsRuntimeConfigurer configurer) {
configurer.eventHandler(new SampleHandler());
}
}Load Plugin Code with the Spring Component Model
In case your reuse module depends on other components managed as part of the Spring ApplicationContext (having an @Autowired annotation in your class is a good hint for that) you need to register your plugin as a Spring component itself. The most straight forward (but not recommended) way is to annotate the plugin class itself with @Component.
This is, however, error-prone: Spring Boot's component scan is by default scanning downward from the package in which the main Application class is declared. Meaning that you need to place the plugin either in a subpackage or the same package as the Application class. This would hamper the reuse aspect of the plugin as it would only work applications in a specific package. You could customize the component scan of the application using your plugin but this is also error-prone as you explicitly have to remember to change the @ComponentScan annotation each time you include a plugin.
Because of those complications it's best practice to use the AutoConfiguration mechanism provided by Spring Boot in reuse modules that ship Spring components. For further details, please refer to the Spring Boot reference for auto-configuration.
A complete end-to-end example for reusable event handlers can be found in this blog post.
Custom Protocol Adapters
In CAP, the protocol adapter is the mechanism to implement inbound communication (from another service or the UI) to the CAP service in development. You can read more about protocol adapters in our architecture documentation.
Usually, a protocol adapter comes in two parts:
- the adapter
- a factory class that creates an instance of the adapter
The adapter itself is in most cases an extension of the HttpServlet abstract class. The factory class also provides information about the paths to which the protocol adapter (the servlet) needs to be registered. The factory interface is called ServletAdapterFactory and implementations of that factory will be loaded with the same ServiceLoader approach as described above in the event handler section.
This is an example implementation of the ServletAdapterFactory:
public class SampleAdapterFactory implements ServletAdapterFactory, CdsRuntimeAware {
/*
* a short key identifying the protocol that's being served
* by the new protocol adapter, for example, odata-v4, hcql, ..
*/
static final String PROTOCOL_KEY = "protocol-key";
private CdsRuntime runtime;
@Override
public void setCdsRuntime(CdsRuntime runtime) {
/*
* In case the protocol adapter needs the CdsRuntime
* the factory can implement CdsRuntimeAware and will
* be provided with a CdsRuntime via this method.
* The create() method below can then use the provided
* runtime for the protocol adapter.
*/
this.runtime = runtime;
}
@Override
public Object create() {
// Create and return the protocol adapter
return new SampleAdapter(runtime);
}
@Override
public boolean isEnabled() {
// Determines if the protocol adapter is enabled
}
@Override
public String getBasePath() {
// Return the base path
}
@Override
public String[] getMappings() {
/*
* Return all paths to which the protocol adapter is
* going to be mapped. Usually, this will be each CDS
* service with either it's canonical or annotated
* path prefixed with the base path of the protocol
* adapter (see above).
*/
}
@Override
public UrlResourcePath getServletPath() {
/*
* Use the UrlResourcePathBuilder to build and return
* a UrlResourcePath containing the basePath (see above)
* and all paths being registered for the protocol key
* of the new protocol adapter.
*/
}
}With the factory in place, you can start to build the actual protocol adapter. As mentioned before, most adapters implement HTTP connectivity and are an extension of the Jakarta HttpServlet class. Based on the incoming request path the protocol adapter needs to determine the corresponding CDS ApplicationService. Parts of the request path together with potential request parameters (this depends on the protocol to be implemented) then need to be mapped to a CQL statement, which is then executed on the previously selected CDS ApplicationService.
public class SampleAdapter extends HttpServlet {
private final CdsRuntime runtime;
public SampleAdapter(CdsRuntime runtime) {
this.runtime = runtime;
// see below for further details
}
@Override
public void service(HttpServletRequest request,
HttpServletResponse response) throws IOException {
// see below for further details
}
}As mentioned previously, a protocol adapter maps incoming requests to CQL statements and executes them on the right ApplicationService according to the HttpServletRequest's request-path. In order to have all relevant ApplicationServices ready at runtime, you can call runtime.getServiceCatalog().getServices(ApplicationService.class) in the adapter's constructor to load all ApplicationServices. Then select the ones relevant for this protocol adapter to have them ready, for example in a Map, for serving requests in service().
When handling incoming requests at runtime, you need to extract the request path and parameters from the incoming HttpServletRequest. Then, you can use CQL API from the cds4j-api module to create CQL corresponding to the extracted information. This statement then needs to be executed with ApplicationService.run(). The returned result then needs to be mapped to the result format that is suitable for the protocol handled by the adapter. For REST, it would be some canonical JSON serialization of the returned objects.
REST request:
GET /CatalogService/Books?id=100Resulting CQL statement:
CqnSelect select = Select.from("Books").byId(100);The CqnSelect statement can then be executed with the right (previously selected) ApplicationService and then written to HttpServletResponse as a serialized string.
String resposePayload = applicationService.run(select).toJson();
response.getWriter().write(responsePayload);With that, a first iteration of a working CAP Java protocol adapter would be complete. As a wrap-up, this would be the tasks that need to be implemented in the adapter:
- Extract the request path and select the corresponding CDS
ApplicationService. - Build a CQL statement based on the request path and parameters.
- Execute the CQL statement on the selected service and write the result to the response.
One final comment on protocol adapters: even a simple protocol adapter like sketched in this section enables full support of other CAP features like declarative security, i18n and of course custom as well as generic event handlers.
Plugin Conventions & Reference Implementations
The following sections document the patterns followed by the plugins in the cap-java GitHub organization. They are presented here to help you write a plugin that fits naturally into the ecosystem. As noted above, use your own groupId — not com.sap.cds.
Reference Plugins
The table below maps each major pattern to a plugin that demonstrates it well. Use these as living examples while reading the sections that follow.
| Pattern | Reference plugin |
|---|---|
| Minimal event-handler plugin (annotation-driven, no model) | cap-plugin-for-sap-document-ai (plugin-template-emoji skeleton) |
Full service model + custom Service + mock/productive switch | cds-feature-print |
| Annotation-driven handler + binding-driven enablement | cds-feature-auditlog-ng |
environment() hook to inject remote-service config | cds-ai |
| Reusable CDS model + aspects + i18n + code-list CSV | cds-feature-attachments |
Spring AutoConfiguration + typed @ConfigurationProperties | cds-feature-n8n |
| Multiple SPIs: servlet adapter + ORD contribution | cds-feature-ord, cds-feature-event-hub |
Recommended Project Layout
Use a multi-module Maven reactor with a root aggregator POM and a dedicated plugin JAR module. Larger plugins add integration-tests and coverage-report modules.
my-plugin-root/ ← aggregator (packaging=pom, artifactId=cds-feature-myplugin-root)
pom.xml
cds-feature-myplugin/ ← the plugin JAR (artifactId=cds-feature-myplugin)
pom.xml
package.json ← pins @sap/cds-dk for the cds-maven-plugin build
src/main/java/
com/example/myplugin/
configuration/ ← CdsRuntimeConfiguration implementation
service/ ← optional: custom Service interface + impl
handler/ ← EventHandler implementations
src/main/resources/
META-INF/services/ ← ServiceLoader registration file
cds/com.example/cds-feature-myplugin/ ← optional: CDS model
integration-tests/ ← optional: test-app Maven module
pom.xml
src/main/… ← runnable CAP app depending on cds-feature-myplugin
coverage-report/ ← optional: JaCoCo aggregation modulePlace plugin implementation classes under a Java package that corresponds to your Maven groupId, for example com.example.myplugin.
Maven Skeleton
Use CI-friendly versioning (${revision}) with the flatten-maven-plugin so your version is set on the command line (mvn … -Drevision=1.0.0) without having to maintain it in every module POM. Import the cds-services-bom in <dependencyManagement> and declare cds-services-api without an explicit version.
<properties>
<revision>0.0.1-SNAPSHOT</revision>
<cds.services.version>{{ versions.java_services }}</cds.services.version>
<java.version>17</java.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.sap.cds</groupId>
<artifactId>cds-services-bom</artifactId>
<version>${cds.services.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<configuration><updatePomFile>true</updatePomFile>
<flattenMode>resolveCiFriendliesOnly</flattenMode></configuration>
<executions>
<execution><id>flatten</id><phase>process-resources</phase>
<goals><goal>flatten</goal></goals></execution>
<execution><id>flatten.clean</id><phase>clean</phase>
<goals><goal>clean</goal></goals></execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<executions>
<execution><id>enforce</id><goals><goal>enforce</goal></goals>
<configuration><rules>
<banDuplicatePomDependencyVersions/>
<requireJavaVersion><version>[17,)</version></requireJavaVersion>
</rules></configuration>
</execution>
</executions>
</plugin>
</plugins>
</build><dependencies>
<dependency>
<groupId>com.sap.cds</groupId>
<artifactId>cds-services-api</artifactId> <!-- no version: managed by BOM -->
</dependency>
<dependency>
<groupId>com.sap.cds</groupId>
<artifactId>cds-services-utils</artifactId>
</dependency>
<!-- test: spin up a real CdsRuntime without Spring -->
<dependency>
<groupId>com.sap.cds</groupId>
<artifactId>cds-services-impl</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>com.sap.cds</groupId>
<artifactId>cds-maven-plugin</artifactId>
<version>${cds.services.version}</version>
<!-- add executions for install-node, generate, etc. when shipping a CDS model -->
</plugin>
</plugins>
</build>{
"name": "cds-feature-myplugin",
"private": true,
"description": "Local @sap/cds-dk for cds-maven-plugin build",
"devDependencies": {
"@sap/cds-dk": "^10"
}
}When your plugin ships a CDS model, add the install-node, npm ci, cds build --for java, and generate executions to the cds-maven-plugin — see the existing Maven plugin reference for details.
Choosing a Registration Hook
All CdsRuntimeConfiguration hooks are optional — override only the ones you need. The table below shows which hook each pattern uses and a reference plugin.
| Hook | Use when | Reference |
|---|---|---|
environment(configurer) | You need to inject a remote-service config or additional CSV data paths into CdsProperties before services start | cds-ai (AICoreServiceConfiguration), cds-feature-notifications |
services(configurer) | Your plugin provides a new CAP service (e.g. PrintService, a messaging service) via configurer.service(...) | cds-feature-print, cds-feature-event-hub |
eventHandlers(configurer) | Your plugin registers event handlers via configurer.eventHandler(...) | cds-feature-auditlog-ng, cds-feature-attachments |
providers(configurer) | Your plugin contributes resource providers (ORD, custom adapters) | cds-feature-ord |
order() | You need to control initialization order relative to other plugins | cds-feature-console (returns Integer.MAX_VALUE) |
Hooks are not mutually exclusive — many plugins override both services() and eventHandlers().
When you need Spring features (typed @ConfigurationProperties, @ConditionalOnProperty, @ConditionalOnMissingBean), use Spring AutoConfiguration instead of — or in addition to — CdsRuntimeConfiguration. Declare the config class in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports and annotate it with @Configuration. The cds-feature-n8n plugin demonstrates this approach with N8nAutoConfiguration.
Custom Services and Event Contexts
When your plugin exposes its own service (rather than adding handlers to existing services), define a Service interface and an implementation, and register both via configurer.service(...) in the services() hook. The interface should declare a DEFAULT_NAME constant that other components can use to look up the service from the ServiceCatalog. If your plugin ships a CDS model and compiles it, you can also use the generated service interface instead of writing one manually.
// Service interface
public interface PrintService extends Service {
String DEFAULT_NAME = "PrintService";
// define actions / event names as constants
String EVENT_PRINT = "print";
}
// Register in CdsRuntimeConfiguration
@Override
public void services(CdsRuntimeConfigurer configurer) {
configurer.service(new PrintServiceImpl(configurer.getCdsRuntime()));
}
@Override
public void eventHandlers(CdsRuntimeConfigurer configurer) {
configurer.eventHandler(new MyPrintHandler());
}For typed event handling, define a custom EventContext interface extending EventContext and annotate handler methods with it:
@ServiceName(value = "*", type = PrintService.class)
public class MyPrintHandler implements EventHandler {
@On(event = PrintService.EVENT_PRINT)
public void onPrint(PrintEventContext context) {
// access strongly-typed request payload
}
}This pattern appears in cds-feature-print (PrintService, PrintEventContext) and cds-feature-attachments (AttachmentService, AttachmentCreateEventContext).
Configuration & Enablement
There is no cds.<feature>.enabled convention across the ecosystem. Enablement is handled in one of two ways:
Binding-driven enablement (ServiceLoader plugins): Check at registration time whether the expected service binding is present. If not, skip registering the handler or service — the plugin becomes a no-op. This keeps consuming apps configuration-free for the common case.
@Override
public void eventHandlers(CdsRuntimeConfigurer configurer) {
boolean hasBinding = configurer.getCdsRuntime()
.getEnvironment().getServiceBindings().stream()
.anyMatch(b -> ServiceBindingUtils.matches(b, "my-service-label"));
if (hasBinding) {
configurer.eventHandler(new ProductionHandler(...));
} else {
configurer.eventHandler(new MockHandler(...)); // dev/test fallback
}
}The mock-vs-productive handler switch based on binding presence is a particularly common pattern: cds-feature-print (BtpPrintHandler vs ConsolePrintHandler), cds-ai (AICoreApiHandler vs MockAICoreApiHandler), cds-feature-notifications (ProductionHandler vs LocalHandler).
Spring conditional wiring: When the plugin uses Spring AutoConfiguration, lean on @ConditionalOnProperty, @ConditionalOnMissingBean, and @ConditionalOnClass to select beans. Pair this with a typed @ConfigurationProperties class so consumers configure the plugin via application.yaml under a stable prefix. See cds-feature-n8n (N8nProperties, N8nAutoConfiguration) for a complete example.
In both cases, if optional functionality depends on a dependency that may not be on the classpath (for example, an SAP Cloud SDK class), guard the code with @ConditionalOnClass or a programmatic ClassUtils.isPresent(...) check so a missing dependency causes a clean "feature disabled" outcome, not a NoClassDefFoundError.
To avoid taking a dependency on live external services (SAP BTP Alert Notification, print service, and so on), replace external calls with CDS mock handlers. The mock handler implements the same service interface as the production adapter but returns canned responses:
@ServiceName(value = "*", type = MyExternalService.class)
public class MyExternalServiceMockHandler implements EventHandler {
@On(event = "*")
public void handleAll(EventContext ctx) {
ctx.setResult(/* canned response */);
ctx.setCompleted();
}
}Register the mock in the test app's CdsRuntimeConfiguration (or as a @Component in the Spring test context) and activate it when the production binding is absent. This is a very common pattern for CAP plugins as well as core parts of the CAP framework. This enables local development as well as efficient testing without the need to adapt the application code.
Testing a Plugin
A plugin unit test can boot a real CdsRuntime without Spring by putting cds-services-impl on the test classpath and calling CdsRuntime.create(...) directly. This keeps tests fast and free of Spring context overhead.
<dependency>
<groupId>com.sap.cds</groupId>
<artifactId>cds-services-impl</artifactId>
<scope>test</scope>
</dependency>Local testing with the plugin: Add a dedicated integration-tests Maven module — a minimal CAP Spring Boot application (Application.java, a small test-service.cds, application.yaml) that depends on the plugin JAR via ${revision}. This lets consumers verify the plugin locally before publishing. Running it under maven-failsafe-plugin (**/*IT.java) exercises the plugin in a real application context.
Key configuration for the test application:
# integration-tests/src/main/resources/application.yaml
spring:
sql:
init:
platform: h2
cds:
security:
mock:
users:
- name: test-user
roles: [admin]Repository Hygiene
The plugins in cap-java share a common set of repository conventions worth adopting:
- Licensing: Apache 2.0, with REUSE-compliant
REUSE.tomlandLICENSES/Apache-2.0.txt. Source files carry a standard SAP copyright header enforced by Spotless or Checkstyle. - CI/CD: GitHub Actions workflows for PR builds, main-branch builds, and release publishing. Publishing to Maven Central uses
central-publishing-maven-plugin(Sonatype Central Portal) with GPG signing under adeploy-releaseMaven profile. - Dependency maintenance: Dependabot or Renovate keeps BOM and build-tool versions current.
As you've learned in this guide, there are various ways to extend the CAP Java framework. You can use one or more of the mentioned techniques and combine them in one or more Maven modules. This totally depends on your needs and requirements.
Most probably you combine the Event Handler with custom types and annotations mechanism together with Sharing reusable CDS models via Maven artifacts because the event handler mechanism might rely on shared CDS artifacts. The protocol adapters on the other hand are generic and model-independent modules that should be packaged and distributed independently.