This plugin for the SAP Cloud Application Programming Model (CAP) provides a caching service to improve performance in CAP applications. While CAP in general performs well for most use cases, caching can help with:
- Slow remote service calls
- Complex calculations
- Heavy queries
- External API integration
While caching can help with these, it also adds complexity and should be used judiciously.
Please also read the introduction blog post in the SAP Community: Boosting performance in SAP Cloud Application Programming Model (CAP) applications with cds-caching.
It's important to understand the difference between caching and data replication:
-
Caching temporarily stores data to reduce latency and improve response times. It's ideal for read-heavy workloads but does not maintain data integrity or understand data semantics.
-
Replication creates full, persistent copies of remote data within your application to ensure availability and enable seamless data sharing across systems. It focuses on resilience rather than performance optimization.
cds-caching is specifically designed for efficient caching, not data replication.
- Flexible Key-Value Store – Store and retrieve data using simple key-based access.
- CachingService – A cds.Service implementation with an intuitive API for seamless integration into CAP.
- Event Handling – Monitor and react to cache events, such as before/after storage and retrieval.
- CAP-specific Caching – Effortlessly cache CQN queries or CAP cds.Requests using code or the @cache annotation.
- TTL Support – Automatically manage data expiration with configurable time-to-live (TTL) settings.
- Tag Support – Use dynamic tags for flexible cache invalidation options.
- Pluggable Storage Options – Choose between in-memory caching, SQLite or Redis.
- Compression – Compress cached data to save memory using LZ4 or GZIP.
- Integrated Statistics – Monitor cache performance with hit rates, latencies, and more.
Installing and using cds-caching is straightforward since it's a CAP plugin. Simply run:
npm install cds-caching
Next, add a caching service configuration to your package.json. You can even define multiple caching services, which is recommended if you need to cache different types of data within your application.
{
"cds": {
"requires": {
"caching": {
"impl": "cds-caching",
"namespace": "my::app::caching"
},
// Optional: Define a specific caching service for Business Partner API
"bp-caching": {
"impl": "cds-caching",
"namespace": "my::app::bp-caching"
}
}
}
}
For more control, you can specify additional options:
{
"cds": {
"requires": {
"caching": {
"impl": "cds-caching",
"namespace": "my::app::caching",
"store": "in-memory", // "in-memory" or "sqlite" or "redis"
"compression": "lz4", // "lz4" or "gzip"
"credentials": { // if store is redis or sqlite
// Redis specific
"host": "localhost",
"port": 6379,
"password": "optional",
"url": "redis://..." // Alternative: Redis connection URI
// SQLite specific
"url": "sqlite://./cache.sqlite"
"table": "cache",
"busyTimeout": 10000
},
"statistics": {
"enabled": true,
"persistenceInterval": 60000, // Optional: Interval for statistics persistence
"maxLatencies": 1000 // Optional: Maximum number of latencies to track
}
}
}
}
}
cds-caching provides 3 storage options:
- Simple and fast, but not persistent
- Not suitable for production since Node.js runtime memory is limited
- Data is lost when the application restarts
- Memory on SAP BTP Cloud Foundry is limited (up to 16 GB) and produces costs
- Data is stored in local SQLite database
- Data is persited next to SAP BTP application with disk-quota up to 10 GB
- Cache will be removed after each deployment to SAP BTP
- No distributed cache between application instances (horizontal scaling)
- Persistent and supports distributed caching
- Works across multiple app instances, making it ideal for scalable applications
- Available on SAP BTP via hyperscaler options (e.g., AWS, Azure, Google Cloud)
- Even trial accounts provide Redis access
For local development, Redis can be quickly set up using Docker. A simple docker-compose configuration provides a lightweight caching environment:
- Create a
docker-compose.yml
file:
services:
redis:
image: redis:latest
container_name: local-redis
ports:
- "6379:6379"
- Run Redis with:
docker compose up -d
- Modify the
package.json
configuration to connect to the local Redis instance:
{
"cds": {
"requires": {
"caching": {
"impl": "cds-caching",
"namespace": "myCache",
"store": "redis",
"[development]": {
"credentials": {
"host": "localhost",
"port": 6379
}
}
}
}
}
}
Now, caching will be handled by Redis instead of in-memory storage during development.
For production deployments on SAP BTP, Redis can be provisioned as a managed service through the Redis on SAP BTP hyperscaler option. An instance can be provisioned via trial or even as a Free Tier to explore the service. However, for production scenarios the size of the Redis instance should match your caching requirements.
To bind Redis to your CAP application on SAP BTP, add the following configuration in mta.yaml
. This will automatically create the service instance and bind your application to it. Since the credentials will automatically be fetched by CAP, make sure to maintain the service-tags to match the kind property of your cds-caching service(s) in the package.json:
modules:
- name: cap-app-srv
# ... other module configuration ...
requires:
- name: redis-cache
resources:
- name: redis-cache
type: org.cloudfoundry.managed-service
parameters:
service: redis-cache
service-plan: trial
service-tags:
# Must match the kind property in the package.json
- cds-caching
👉 Tip: There is a detailed blog series on Redis in SAP BTP explaining how to set up Redis and connect via SSH for local/hybrid testing, as this is by default not possible.
The caching service provides a flexible API for caching data in CAP applications. Here are the key usage patterns:
The most basic way to use cds-caching is through its key-value API:
// Connect to the caching service
const cache = await cds.connect.to("caching")
// Store a value (can be any object)
await cache.set("key", "value")
// Retrieve the value
await cache.get("key") // => value
// Check if the key exists
await cache.has("key") // => true/false
// Delete the key
await cache.delete("key")
// Clear the whole cache
await cache.clear()
For more advanced CAP integration, cache CAP's CQN queries directly. By passing in the query, a dynamic key is generated based on the CQN structure of the query. Note, that passing in queries with dynamic parameters (e.g. SELECT.from(Foo).where({id: 1})
) will result in a different key for each query execution.
// Create and execute a CQN query
const query = SELECT.from(Foo)
const result = await db.run(query)
// Cache the result
await cache.set(query, result)
// Retrieve from cache using the same query
const cachedResult = await cache.get(query)
Handling the cache manually via read-aside pattern is possible, but the caching service provides a more convenient way to cache and retrieve CQN queries. By using the run
method, the caching service will transparently cache the result of the query and return the cached result if available for all further requests.
const query = SELECT.from(Foo)
// Runs the query internally and caches the result
const result = await cache.run(query, db)
This will transparently cache the result of the query and return the cached result if available for all further requests.
Cache entire CAP requests with context awareness (e.g. user, tenant, locale, etc.), which is useful for caching slow remote service calls or even application services. The caching service will automatically generate a key for the request based on the request object and the current user, tenant and locale.
// Cache the requests to an exposed external entity
this.on('READ', BusinessPartners, async (req, next) => {
const bupa = await cds.connect.to('API_BUSINESS_PARTNER')
let value = await cache.get(req)
if(!value) {
value = await bupa.run(req)
await cache.set(req, value, { ttl: 3600 })
}
return value
})
Alternatively use read-through caching via the run
method to let the caching service handle the caching transparently:
this.on('READ', BusinessPartners, async (req, next) => {
const bupa = await cds.connect.to('API_BUSINESS_PARTNER')
return await cache.run(req, bupa)
})
This will transparently cache the result of the request and return the cached result if available for all further requests.
Caching an entire entity should be used with caution, as it will cache all permutations of requests ($filter, $expand, $orderby, etc.) on the entity, which will lead to a huge number of cache entries. Use this only for entities where you can guarantee a low number of different queries.
But not only external services can be cached, it's also possible to cache requests against an ApplicationService.
Here, you should make use of the prepend
function, to register the on
handler before the default handler. Thus, it is possible to first check for the cache entries and only execute the default behavior if necessary.
class MyService extends cds.ApplicationService {
async init() {
// Read-through caching for the full entity
this.prepend(() => {
const { MyEntity } = this.entities;
this.on('READ', MyEntity, async (req, next) => {
const cache = cds.connect.to("caching");
return cache.run(req, next);
});
});
return super.init()
}
}
Alternatively to doing this via code, you can use annotations to enable caching on service entities or OData functions. The caching service will automatically generate a key for the request based on the request object and the current user, tenant and locale.
service MyService {
@cache: {
ttl: 3600
}
entity BusinessPartners as projection on BusinessPartner {
// ... entity definition
}
@cache: {
ttl: 1800,
tags: [{
template: 'user-{user}'
}]
}
function getUserPreferences() returns array of Preferences;
}
While not directly related to CAP functionality, the caching service provides two methods for read-through caching of JavaScript functions:
// Using wrap() to create a cached version of a function
const expensiveOperation = async (value) => {
// ... some expensive computation
return result
}
// Creates a cached version of the function
const cachedOperation = cache.wrap("key", expensiveOperation, {
ttl: 3600,
tags: ['computation']
})
// Each call checks cache first, only executes if cache miss
const result = await cachedOperation("input")
// Using exec() for immediate execution with caching
const result = await cache.exec("key", async () => {
// ... some expensive computation
return result
}, {
ttl: 3600,
tags: ['computation']
})
The key differences between wrap()
and exec()
:
-
wrap()
returns a new function that includes caching logic -
exec()
immediately executes the function and caches the result - Use
wrap()
when you need to reuse the cached function multiple times - Use
exec()
for one-off executions with caching
The caching service provides different strategies to invalidate cached values.
IMPORTANT: You should not use cds-caching without a proper invalidation strategy.
The most basic strategy is to use a time-to-live (TTL) for the cache. The caching service will automatically delete the value from the cache after the specified TTL has expired.
The TTL can be specified for individually through all cache methods (e.g. set
, run
, send
, wrap
, exec
).
// Store with 60 seconds TTL
await cache.set("key", "value", { ttl: 60000 })
// Run with 30 seconds TTL
const result = await cache.run(query, db, { ttl: 30000 })
// Send with 10 seconds TTL
const result = await cache.send(request, service, { ttl: 10000 })
// Wrap with 10 seconds TTL
const cachedOperation = cache.wrap("key", expensiveOperation, { ttl: 10000 })
// Exec with 10 seconds TTL
const result = await cache.exec("key", async () => {
// ... some expensive computation
return result
}, {
ttl: 10000
})
Key-based invalidation is a way to invalidate cache entries based on a specific key.
await cache.delete("key")
Keys are critical for cache invalidation. To allow custom key management, you can override the auto-generated key. This option is available for all essential methods (e.g cache.set, cache.run, cache.send, cache.createKey) and for the annotations.
// No key override given, string will just be used as keys
await cache.set('key', 'value') // key: key
// No key override given, objects will be smartly hashed
await cache.set(SELECT.from(Foo)) // key: bd3f3690d3e96a569bd89d9e207a89af
// Automatically build the key for retrieval/deletion
cache.createKey(SELECT.from(Foo)) // key: bd3f3690d3e96a569bd89d9e207a89af
// Override and use your own key based on a fixed value
await cache.set(SELECT.from(Foo, 1), { key: { value: "foo:1" } })
// Override and only for requests, use request context information
await cache.run(req, remoteService, { key: { template: "mykey:{tenant}:{user}:{locale}:{hash}" } })
// This requests will be cached for all users and for each locale
await cache.set(req, remoteService, { key: { template: "mykey:{user}:{locale}:{hash}" } })
Overriding keys support the following configuration options:
-
value
– generates a static value -
prefix
– will add this piece at the beginning -
suffix
- will ad this piece at the end -
template
- will set a value filled with placeholders, available placeholders are (only relevant for cds.Requests):-
{user}
: The current user -
{tenant}
: The current tenant -
{locale}
: The current locale -
{hash}
: The hash of the request query/params/data/path/etc.
-
With well-structured keys, invalidating cache entries becomes a lot easier. However, for more complex scenarios tags provide an even more effective solution, as tags can automatically be created based on the cached data.
Tags are a way to invalidate cache entries based on a specific tag. Tags need to be provided explicitly when storing a value in the cache and are supported for all cache methods (e.g. set
, run
, send
, wrap
, exec
).
Tags can be provided as an array of strings or as an array of objects with the following properties:
-
value
: The value to use for the tag. -
data
: A field from the value to use for the tag. This is working for objects and arrays of objects. -
prefix
: A prefix that will be added to the tag. -
suffix
: A suffix that will be added to the tag. -
template
: A template string that will be used to generate the tag (e.g.{tenant}-{locale}-{user}-{hash}
). This is useful for dynamic tags based on cds.Requests. Templates support the following properties:-
{user}
: The current user -
{tenant}
: The current tenant -
{locale}
: The current locale -
{hash}
: The hash of the current query
-
// Store with static tag
await cache.set("key", "value", {
tags: [{ value: "user-123" }]
})
// Store with template tag (will generate a tag like "tenant-global-user-anonymous")
await cache.set("key", "value", {
tags: [{ template: "tenant-{tenant}-user-{user}" }]
})
// Store with data-based tag
await cache.set("key", { id: 123, name: "Product" }, {
tags: [{ data: "id", prefix: "product-" }]
})
// Invalidate by tag
await cache.deleteByTag('user-123')
This is really useful for invalidating cache entries based on a specific attribute or context.
Dynamic tags using data data
property are a way to invalidate cache entries based on the data itself. The caching service will automatically generate a tag for the value and invalidate the cache entry when the value changes.
const businessPartners = [
{
businessPartner: 1,
name: 'John Doe'
},
{
businessPartner: 2,
name: 'Jane Doe'
}
]
// Store with dynamic tags
await cache.set("key", businessPartners, {
tags: [
{ data: 'businessPartner', prefix: 'bp-' },
{ value: "businessPartner" }
]
})
// Introspect the tags
const tags = await cache.tags("key") // => ["bp-1", "bp-2", "businessPartner"]
// Invalidate by tag
await cache.deleteByTag('bp-1')
await cache.deleteByTag('bp-2')
This is really usefull for caching results with multiple rows where you can't predict the tags beforehand or when you want to invalidate cache entries based on the data itself. This is also possible for the run
method.
const result = await cache.run(query, db, {
tags: [{ data: 'businessPartner', prefix: 'bp-' }]
})
This will transparently cache the result of the query and create a tag for each business partner in the result. If you use the same technique in other places and you want to invalidate the cache entries for a specific business partner, you can do this by simply invalidating the tag bp-1
.
The caching service provides an iterator interface to traverse all cache entries:
const iterator = await cache.iterator()
for await (const entry of iterator) {
console.log(entry)
}
This will return an iterator over all cache entries. You can use this to traverse all cache entries and invalidate them based on a specific condition. You should only use this for small caches (e.g. by using multiple caching services with different namespaces).
While caching individual requests can improve performance, caching an entire OData service is generally not recommended. Here's why:
-
Data Consistency: OData services expose live business data that frequently changes. Caching responses without an appropriate invalidation strategy can lead to outdated or incorrect data being served.
-
Query Complexity: OData allows dynamic query parameters ($filter, $expand, $orderby, etc.), making it difficult to cache efficiently without storing excessive variations.
-
Payload Size: Full OData responses can be significantly large, consuming cache memory inefficiently compared to caching targeted CQN queries or specific request results.
Instead of caching entire OData service responses, focus on:
- Specific queries or request results
- Static master data
- Computed results
- Remote service calls with stable data
-
Cache Selectively: Not all data benefits from caching. Focus on:
- Frequently accessed, rarely changed data
- Computationally expensive operations
- Remote service calls with stable data
-
Use Appropriate TTLs: Set TTLs based on data volatility:
- Short TTLs (seconds/minutes) for frequently changing data
- Longer TTLs (hours/days) for stable reference data
-
Implement Cache Tags: Use tags for granular cache invalidation:
- Group related cache entries
- Enable targeted invalidation
- Use dynamic tags for user/tenant-specific caching
-
Monitor Cache Performance: Regularly check cache statistics:
- Hit rates
- Memory usage
- Response times
- Error rates
- Memory Usage: Monitor cache size, especially with in-memory storage
- Consistency: Consider data freshness requirements when setting TTLs
- Multi-Tenant: Use appropriate namespacing and key strategies
- Redis Setup: Ensure proper configuration for production use
Creates a key from a string or an object. This method is used internally when passing keys to the cache methods, so you don't need to call it directly other then to retrieve the dynamic generated key for a given object.
The key to create the key from. The key can be a string or an object. If an object is used, it will be hashed to a string key using MD5. cds.Requests are handled explicitly as the dynamic generated key includes the user, tenant and locale and query hash.
A string key.
Sets a value in the cache.
The key to store the value under. The key handling is the same as for the ≈
method.
The value to store in the cache. The value will be serialized to a string using JSON.stringify
(unless the value is already a string).
Object literal containing cache options.
The following properties are accepted:
Property | Description | Example |
---|---|---|
ttl | Time-to-live in milliseconds. | 1000 |
key | Key override for the cache for full control over the key management (see chapter Cache Invalidation Strategies) | {template: 'user-{user}', value: '123'} |
tags | Array of tags to associate with the value. Tags can be dynamic based on the stored cache data (see chapter Cache Invalidation Strategies) | [{template: 'user-{user}', value: '123'}] |
Gets a value from the cache.
The key to retrieve the value from. The key handling is the same as for the createKey
method.
The deserialized value from the cache or undefined
if the value does not exist.
Checks if a value exists in the cache.
The key to check for existence. The key handling is the same as for the createKey
method.
true
if the value exists in the cache, false
otherwise.
Deletes a value from the cache.
The key to delete the value from. The key handling is the same as for the createKey
method.
Clears the whole cache.
Deletes all values from the cache that are associated with the given tag.
The tag to delete the values from.
Runs a query against the provided service and caches the result for all further requests. This method is useful for read-through caching. (see Usage Patterns and CAP docs for more information)
The CQN query to run.
The service to run the query on.
The result of the query, either from the cache or the service.
Sends a request to a cds.Service and caches the result. In contrast to the run
method, this method is useful for caching full cds.Requests.
The request to send.
The service to send the request to.
The result of the request, either from the cache or the service.
Wraps a function in a cache.
The key to store the cached function under. The key handling is the same as for the createKey
method.
The async function to wrap in a cache.
The options to use for the cache.
A cached version of the function. The cached function will check the cache first and only execute the function if the cache miss.
Executes a function and caches the result. This method is useful for one-off executions with caching.
The key to store the cached function under. The key handling is the same as for the createKey
method.
The async function to execute.
The options to use for the cache.
The result of the function.
await cache.iterator() : AsyncIterator<{ key: string, value: { value: any, tags: string[], timestamp: number } }>
Returns an iterator over all cache entries.
An iterator over all cache entries.
Returns the tags for a given key.
The key to get the tags for. The key handling is the same as for the createKey
method.
An array of tags. If the key does not exist, an empty array is returned.
Returns the metadata for a given key.
The key to get the metadata for. The key handling is the same as for the createKey
method.
An object containing the metadata for the given key or undefined
if the key does not exist. The metadata object contains the following properties:
-
tags
: An array of tags. -
timestamp
: The timestamp of the cache entry.
Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository.
This project is licensed under the MIT License - see the LICENSE file for details.