logo

Are you need IT Support Engineer? Free Consultant

Change Tracking in SAP CAP Java: Audit Trails Without the Boilerplate

  • By sujay
  • 03/09/2026
  • 30 Views

Introduction

Knowing who changed a record, and what it contained before the change, is a recurring requirement in enterprise applications. Auditors need it for compliance, support teams rely on it to diagnose issues, and organizations depend on it to maintain data transparency and accountability.

Traditionally, meeting this requirement means writing custom audit logic — comparing old and new values, maintaining separate history tables, and keeping that code in step with an evolving data model. The SAP Cloud Application Programming Model (CAP) offers a more efficient alternative: a built-in change tracking feature that records every create, update, and delete operation automatically, driven entirely by declarative annotations.

This guide walks you through implementing change tracking in a CAP Java application using a simple Order Management scenario.

What We Will Build

A simple Order Management application that automatically records changes whenever an order is created, updated, or deleted. The application will track:

  • Customer name
  • Order status
  • Total amount

It will also store a customer email – which we will deliberately exclude from tracking, since it is personal data (PII).


Project Structure

cap-java-change-tracking/
├── db/
│ └── schema.cds
├── srv/
│ ├── service.cds
│ └── change-tracking.cds
└── pom.xml


Step 1: Define the Data Model

The data model describes the structure of the Orders entity.

File:  db/schema.cds

namespace my.orders;

using { cuid, managed } from ‘@sap/cds/common';

entity Orders : cuid, managed {
orderNumber : String;
customerName : String;
customerEmail : String;
status : String;
totalAmount : Decimal(15,2);
}

annotate Orders with {
customerEmail @PersonalData.IsPotentiallyPersonal;
}

The cuid and managed aspects from @SAP/cds/common automatically add a UUID key along with standard audit fields, including createdAt, createdBy, modifiedAt, and modifiedBy. The remaining fields represent the business-specific data.

The customerEmail field is annotated with @PersonalData.IsPotentiallyPersonal as it contains personally identifiable information (PII). As a result, SAP CAP excludes this personal data from the change log, as we will demonstrate while testing the application.


Step 2: Configure Change Tracking

With the model in place, we declare which entity and fields to track – using annotations in a dedicated file, with no Java code involved.

File: srv/change-tracking.cds

using { sap.changelog as changelog } from ‘com.sap.cds/change-tracking';
using my.orders as db from ‘../db/schema';

// Enable change tracking on the Orders entity
extend my.orders.Orders with changelog.changeTracked;

// Specify the fields to track
annotate my.orders.Orders @changelog: [orderNumber] {
customerName @changelog;
status @changelog;
totalAmount @changelog;
};

Explanation:

AnnotationPurpose
changelog.changeTracked Enables change tracking on the entity
@changelog: [orderNumber] Uses orderNumber to identify records in the change log
@changelog Marks specific fields for tracking

We intentionally omit @changelog on customerEmail – as a @PersonalData field, it is excluded regardless.


Step 3: Add the Change Tracking Dependency

File: pom.xml

<dependency>
<groupId>com.sap.cds</groupId>
<artifactId>cds-feature-change-tracking</artifactId>
<scope>runtime</scope>
</dependency>

Once the dependency is added to the classpath, the feature automatically registers its handlers with the CAP runtime and processes the annotations defined in Step 2. No additional handler implementation is required.

The application can then be built.

mvn clean install


Step 4: Create the Service Definition

Expose the Orders entity and the change log through an OData service.
File: srv/service.cds

using my.orders as db;
using { sap.changelog.Changes } from ‘com.sap.cds/change-tracking';

service OrderService {
entity Orders as projection on db.Orders;

@readonly
entity ChangeLog as projection on Changes;
}

EntityPurpose
OrdersFull CRUD operations on orders
ChangeLogRead-only access to the change history

Marking ChangeLog as @readonly ensures the audit trail can't be altered through the service.


Step 5: Run the Application

mvn spring-boot:run

 

Step 6: Test the Change Tracking

6.1 Create an Order

curl -X POST http://localhost:8080/odata/v4/OrderService/Orders \
-H “Content-Type: application/json” \
-d ‘{
“orderNumber”: “ORD-001”,
“customerName”: “John Doe”,
“customerEmail”: “john.doe@example.com”,
“status”: “pending”,
“totalAmount”: 100.00
}'

Response:

{
“@context”: “$metadata#Orders/$entity”,
“ID”: “3caae820-892f-427a-9ca7-2c0726376d3a”,
“createdAt”: “2026-09-02T06:31:28.499629Z”,
“createdBy”: “anonymous”,
“modifiedAt”: “2026-09-02T06:31:28.499629Z”,
“modifiedBy”: “anonymous”,
“orderNumber”: “ORD-001”,
“customerName”: “John Doe”,
“customerEmail”: “john.doe@example.com”,
“status”: “pending”,
“totalAmount”: 100
}

The order is created with a generatedID, themanagedaudit fields are populated automatically, and change tracking records the initial values of the tracked fields. Note thatcustomerEmailis stored on the order but, being personal data, isnotrecorded in the change log. ThecreatedByandmodifiedByfields showanonymous, as no authentication is configured in this example.


6.2 View Change Log (After Create)

curl http://localhost:8080/odata/v4/OrderService/ChangeLog

Response:

{
“@context”: “$metadata#ChangeLog”,
“value”: [
{
“changeLogID”: “cac08b38-49ab-494b-a5e4-80a3cd1267e6”,
“rootEntity”: “OrderService.Orders”,
“rootIdentifier”: “ORD-001”,
“attribute”: “customerName”,
“valueChangedFrom”: null,
“valueChangedTo”: “John Doe”,
“valueDataType”: “cds.String”,
“modification”: “create”,
“modificationText”: “Create”,
“createdAt”: “2026-09-02T06:31:28.560396Z”,
“createdBy”: “anonymous”
},
{
“changeLogID”: “cac08b38-49ab-494b-a5e4-80a3cd1267e6”,
“rootEntity”: “OrderService.Orders”,
“rootIdentifier”: “ORD-001”,
“attribute”: “totalAmount”,
“valueChangedFrom”: null,
“valueChangedTo”: “100.00”,
“valueDataType”: “cds.Decimal”,
“modification”: “create”,
“modificationText”: “Create”,
“createdAt”: “2026-09-02T06:31:28.560396Z”,
“createdBy”: “anonymous”
},
{
“changeLogID”: “cac08b38-49ab-494b-a5e4-80a3cd1267e6”,
“rootEntity”: “OrderService.Orders”,
“rootIdentifier”: “ORD-001”,
“attribute”: “status”,
“valueChangedFrom”: null,
“valueChangedTo”: “pending”,
“valueDataType”: “cds.String”,
“modification”: “create”,
“modificationText”: “Create”,
“createdAt”: “2026-09-02T06:31:28.560396Z”,
“createdBy”: “anonymous”
}
]
}

The log holds three entries – one per tracked field. Each shows valueChangedFrom: null (a new record), valueChangedTo set to the initial valuemodification: create, and rootIdentifier: ORD-001 for easy identification. As expected, there is no entry for customerEmail – confirming the PII exclusion from Step 1.

In this example the createdBy field shows anonymous, as no authentication has been configured for the application.

6.3 Update the Order

curl -X PATCH “http://localhost:8080/odata/v4/OrderService/Orders(3caae820-892f-427a-9ca7-2c0726376d3a)” \
-H “Content-Type: application/json” \
-d ‘{
“status”: “shipped”,
“totalAmount”: 150.00,
“customerEmail”: “john.new@example.com”
}'

Response:

{
“@context”: “$metadata#Orders/$entity”,
“ID”: “3caae820-892f-427a-9ca7-2c0726376d3a”,
“createdAt”: “2026-09-02T06:31:28.499629Z”,
“createdBy”: “anonymous”,
“modifiedAt”: “2026-09-02T06:38:50.917910Z”,
“modifiedBy”: “anonymous”,
“orderNumber”: “ORD-001”,
“customerName”: “John Doe”,
“customerEmail”: “john.new@example.com”,
“status”: “shipped”,
“totalAmount”: 150
}

All three fields are updated on the entity – including customerEmail, which now reads john.new@example.com

6.4 View Change Log (After Update)

curl http://localhost:8080/odata/v4/OrderService/ChangeLog

Response:

{
“@context”: “$metadata#ChangeLog”,
“value”: [
{
“changeLogID”: “dd079bfa-d6e0-422e-a3c2-90f13086bc3e”,
“rootIdentifier”: “ORD-001”,
“attribute”: “totalAmount”,
“valueChangedFrom”: “100.00”,
“valueChangedTo”: “150.00”,
“modification”: “update”,
“modificationText”: “Edit”,
“createdAt”: “2026-09-02T06:38:50.944298Z”,
“createdBy”: “anonymous”
},
{
“changeLogID”: “dd079bfa-d6e0-422e-a3c2-90f13086bc3e”,
“rootIdentifier”: “ORD-001”,
“attribute”: “status”,
“valueChangedFrom”: “pending”,
“valueChangedTo”: “shipped”,
“modification”: “update”,
“modificationText”: “Edit”,
“createdAt”: “2026-09-02T06:38:50.944298Z”,
“createdBy”: “anonymous”
},
{
“changeLogID”: “cac08b38-49ab-494b-a5e4-80a3cd1267e6”,
“rootIdentifier”: “ORD-001”,
“attribute”: “totalAmount”,
“valueChangedFrom”: null,
“valueChangedTo”: “100.00”,
“modification”: “create”,
“modificationText”: “Create”,
“createdAt”: “2026-09-02T06:31:28.560396Z”,
“createdBy”: “anonymous”
},
{
“changeLogID”: “cac08b38-49ab-494b-a5e4-80a3cd1267e6”,
“rootIdentifier”: “ORD-001”,
“attribute”: “customerName”,
“valueChangedFrom”: null,
“valueChangedTo”: “John Doe”,
“modification”: “create”,
“modificationText”: “Create”,
“createdAt”: “2026-09-02T06:31:28.560396Z”,
“createdBy”: “anonymous”
},
{
“changeLogID”: “cac08b38-49ab-494b-a5e4-80a3cd1267e6”,
“rootIdentifier”: “ORD-001”,
“attribute”: “status”,
“valueChangedFrom”: null,
“valueChangedTo”: “pending”,
“modification”: “create”,
“modificationText”: “Create”,
“createdAt”: “2026-09-02T06:31:28.560396Z”,
“createdBy”: “anonymous”
}
]
}

What the response shows:

ModificationAttributeFromTo
updatetotalAmount100.00 150.00
updatestatuspendingshipped
createtotalAmountnull100.00
createcustomerNamenullJohn Doe
createstatusnullpending

The significant detail here is what the change log omits. The PATCH request modified three fields — status, totalAmount, and customerEmail – yet only status and totalAmount were recorded. No entry was created for customerEmail, even though the value was successfully updated on the entity (from john.doe@example.com to john.new@example.com).

This behavior demonstrates the effect of the @PersonalData annotation: change tracking excludes personal data from the change log irrespective of the operation performed, and irrespective of any other fields modified within the same request. It is also worth noting that an update operation is represented by the value “Edit” in the modificationText field.

Step 7: Controlling Change Log Retention (Cascade Delete)

By default, change records are retained independently of the entities they describe. When an order is deleted, its change history remains in the log.
For an audit trail this is usually the desired behavior – the history of a record often needs to survive the record itself. In certain situations, however, the change records should be removed together with the entity: to honor a data-cleanup request, for example, or to prevent the log from growing without bound. This is enabled with the @cascade: { delete } annotation.

File: srv/change-tracking.cds

using { sap.changelog as changelog } from ‘com.sap.cds/change-tracking';
using my.orders as db from ‘../db/schema';

extend my.orders.Orders with changelog.changeTracked;

annotate my.orders.Orders @changelog: [orderNumber] {
customerName @changelog;
status @changelog;
totalAmount @changelog;
};

// Delete change records when the tracked Orders entity is deleted
annotate my.orders.Orders.changes:change with @cascade: { delete };

This annotation applies to the Orders entity: when an order is deleted, its accumulated change records are deleted along with it.

Verify the behavior

curl -X DELETE “http://localhost:8080/odata/v4/OrderService/Orders(3caae820-892f-427a-9ca7-2c0726376d3a)”

Now let us view the change log:

curl http://localhost:8080/odata/v4/OrderService/ChangeLog

Response:

{
“@context”: “$metadata#ChangeLog”,
“value”: [
{
“changeLogID”: “d9953608-dae3-4eba-8256-9934d3447e13”,
“rootEntity”: “OrderService.Orders”,
“rootIdentifier”: “ORD-001”,
“attribute”: “customerName”,
“valueChangedFrom”: “John Doe”,
“valueChangedTo”: null,
“valueDataType”: “cds.String”,
“modification”: “delete”,
“modificationText”: “Delete”,
“createdAt”: “2026-09-02T07:59:03.019684Z”,
“createdBy”: “anonymous”
},
{
“changeLogID”: “d9953608-dae3-4eba-8256-9934d3447e13”,
“rootEntity”: “OrderService.Orders”,
“rootIdentifier”: “ORD-001”,
“attribute”: “totalAmount”,
“valueChangedFrom”: “150.00”,
“valueChangedTo”: null,
“valueDataType”: “cds.Decimal”,
“modification”: “delete”,
“modificationText”: “Delete”,
“createdAt”: “2026-09-02T07:59:03.019684Z”,
“createdBy”: “anonymous”
},
{
“changeLogID”: “d9953608-dae3-4eba-8256-9934d3447e13”,
“rootEntity”: “OrderService.Orders”,
“rootIdentifier”: “ORD-001”,
“attribute”: “status”,
“valueChangedFrom”: “shipped”,
“valueChangedTo”: null,
“valueDataType”: “cds.String”,
“modification”: “delete”,
“modificationText”: “Delete”,
“createdAt”: “2026-09-02T07:59:03.019684Z”,
“createdBy”: “anonymous”
}
]
}

What the response shows :

The result is more subtle than the log simply becoming empty:

  • The five previous entries (three creates, two updates) are removed — this is @cascade: { delete } clearing the accumulated history.
  • However, the delete operation itself generates three new delete entries – one per tracked field – each recording the field's final value transitioning to null (John Doe → null, 150.00 → null, shipped → null).

In other words, cascade delete removes the prior change history, while the deletion event is still captured as a final snapshot of what was removed.  As throughout the entity's lifecycle, customerEmail never appears in the log.

Note: Retaining history is the safer default for compliance and troubleshooting. Enable cascade delete only when there is a concrete reason to discard accumulated records – and note that the deletion event itself is still logged.


Key Benefits

  • Minimal Configuration — A dependency and a few annotations, no custom code
  • Automatic Logging — Create, update, and delete are all captured
  • Standardized Format — Consistent change-log structure, exposed as a standard OData entity
  • PII-Aware — Fields marked @PersonalData are automatically excluded
  • Configurable Retention — Choose whether history survives entity deletion


Conclusion

Change tracking is a common requirement across enterprise applications, and the SAP Cloud Application Programming Model addresses it with a capability that is both robust and easy to adopt. As demonstrated in this guide, a fully functional, field-level audit trail can be established with minimal configuration and without any custom implementation.

This declarative approach offers clear advantages: it reduces development effort, ensures consistency as the data model evolves, and provides a standardized, readily accessible record of changes. It also accommodates important real-world concerns, from safeguarding personal data to managing how long historical records are retained.

 

 

 

 

 

 

 

 

 

 

 

Source link

Leave a Reply

Your email address will not be published. Required fields are marked *

Chat with us on WhatsApp!