Skip to content

    Transaction Management

    Transaction management in CAP deals with (ACID) database transactions, principal / context propagation on service-to-service calls and tenant isolation.

    In Essence…
    As an application developer, you don’t have to care about transactions, principal propagation, or tenant isolation at all. CAP runtime manages that for you automatically. Only in rare cases, you need to go beyond that level, and use one or more of the options documented hereinafter.


    Automatic Transactions

    Root Transactions

    Whenever an instance of cds.Service processes requests, the core framework automatically cares for starting and committing or rolling back database transactions, connection pooling, principal propagation and tenant isolation.

    For example a call like that:


    … will cause this to take place on SQL level:

    -- ACQUIRE connection from pool
    CONNECT; -- if no pooled one
    SELECT * from Books;
    -- RELEASE connection to pool

    Service-managed Transactions — whenever a service operation, like above, is executed, the core framework ensures it will either join an existing transaction, or create a new root transaction. Within event handlers, your service always is in a transaction.

    Nested Transactions

    Services commonly process requests in event handlers, which in turn send requests to other services, like in this simplistic implementation of a bank transfer operation:

    const log ='log')
    const db ='db')
    BankingService.on ('transfer', req => {
      let { from, to, amount } =
      await db.update('BankAccount',from).set('balance -=', amount),
      await db.update('BankAccount',to).set('balance +=', amount),
      await log.insert ({ kind:'Transfer', from, to, amount })

    Again, all transaction handling is done by the CAP core framework, in this case by orchestrating three transactions:

    1. A root transaction for BankingService.transfer
    2. A nested transaction for the calls to the db service
    3. A nested transaction for the calls to the log service

    Nested transactions are automatically committed when their root transaction is committed upon successful processing of the request; or rolled back if not.

    No Distributed Transactions — Note that in the previous example, the two nested transactions are synchronized with respect to a final commit / rollback, but not as a distributed atomic transaction. This means, it still can happen, that the commit of one nested transaction succeeds, while the other fails.

    Manual Transactions

    Use cds.tx() to start and commit transactions manually, if you need to ensure two or more queries to run in a single transaction. The easiest way to achive this is shown below:

    cds.tx (async ()=>{
      const [ Emily ] = await db.insert (Authors, {name:'Emily Brontë'})
      await db.insert (Books, { title: 'Wuthering Heights', author: Emily })

    Learn more about cds.tx()

    This usage variant, which accepts a function with nested operations …

    1. creates a new root transaction
    2. executes all nested operations in this transaction
    3. automatically finalizes the transaction with commit or rollback

    Only in non-managed environments — as said above: you don’t need to care for that if you are in a managed environment, i.e., when implementing an event handler. In that case, the core service runtime automatically created a transaction for you already.

    ❗ Warning
    If you’re using the database SQLite, it leads to deadlocks when two transactions wait for each other. Parallel transactions are not allowed and a new transaction is not started before the previous one is finished.

    Background Jobs

    Background jobs are tasks to be executed outside of the current transaction, possibly also with other users, and maybe repeatedly. Use cds.spawn() to to so:

    // run in current tenant context but with privileged user
    // and with a new database transactions each...
    cds.spawn ({ user: cds.User.privileged, every: 1000 /* ms */ }, ()=>{
      const mails = await SELECT.from('Outbox')
      await MailServer.send(mails)
      await DELETE.from('Outbox').where (`ID in ${ => m.ID)}`)

    Learn more about cds.spawn()

    Event Contexts

    Automatic transaction management, as offered by the CAP, needs access to properties of the invocation context — most prominently, the current user and tenant, or the inbound http request object.

    Accessing Context Information

    Access that information anywhere in your code through cds.context like that:

    // Accessing current user
    const { user } = cds.context 
    if ('admin')) ...
    // Accessing http req, res objects
    const { req, res } = cds.context.http 
    if (!'application/json')) res.send(415)

    Learn more about available cds.context properties

    Setting Contexts

    Setting cds.context usually happens in inbound authentication middlewares or in inbound protocol adapters. You can also set it in your code, for example, you might implement a simplistic custom authentication middleware like so:

    app.use ((req, res, next) => {
      const { 'x-tenant':tenant, 'x-user-id':user } = req.headers
      cds.context = { tenant, user } // Setting cds.context

    Continuation-local Variable

    cds.context is implemented as a so-called continuation-local variable.

    As JavaScript is single-threaded, we cannot capture request-level invocation contexts such (as current user, tenant, or locale) in what other languages like Java call thread-local variables. But luckily, starting with Node v12, means for so-called “Continuation-Local Storage (CLS)” were given to us. Basically, the equivalent of thread-local variables in the asynchronous continuations-based execution model of Node.js.

    Context Propagation

    When creating new root transactions in calls to cds.tx(), all properties not specified in the context argument are inherited from cds.context, if set in the current continuation.

    In effect, this means the new transaction demarcates a new ACID boundary, while it inherits the event context properties unless overridden in the context argument to cds.tx(). The following applies:

    cds.context = { tenant:'t1', user:'u1' } === 'u1'          //> true
    let tx = cds.tx({ user:'u2' })
    tx.context !== cds.context            //> true
    tx.context.tenant === 't1'            //> true === 'u2'           //> true
    tx.context.user !== cds.context.user  //> true === 'u1'          //> true

    cds.context cds.EventContext

    The current continuation’s event context. Usually this is set by inbound protocol adaptors or by the top-level service starting to process an event.

    The implementation is a getter/setter pair. The setter coerces values into valid instances of cds.EventContext. For example:

    cds.context = { tenant:'t1', user:'u2' }
    let ctx = cds.context
    ctx instanceof cds.EventContext  //> true
    ctx.user instanceof cds.User     //> true
    ctx.tenant === 't1'              //> true === 'u2'             //> true

    If a transaction object is assigned, it’s tx.context will be used, hence cds.context = tx acts as a convenience shortcut for cds.context = tx.context:

    let tx = cds.context = cds.tx({ ... })
    cds.context === tx.context  //> true

    cds.spawn (options, fn)

    Runs the given function as detached continuation in a specified event context (not inheriting from the current one). Options every or after allow to run the function repeatedly or deferred. For example:

    cds.spawn ({ tenant:'t0', every: 1000 /* ms */ }, async (tx) => {
      const mails = await SELECT.from('Outbox')
      await MailServer.send(mails)
      await DELETE.from('Outbox').where (`ID in ${ => m.ID)}`)

    Even though the callback function is executed as a background job, all asynchronous operations inside the callback function must be awaited. Otherwise, transaction handling does not work properly.


    • options is the same as the context argument for cds.tx(), plus:
      • every: <n> number of milliseconds to use in setInterval(fn,n)
      • after: <n> number of milliseconds to use in setTimeout(fn,n)
      • if non of both is given setImmediate(fn) is used to run the job
    • fn is a function representing the background task


    • An event emitter which allows to register handlers on succeeded, failed, and done events.
      let job = cds.spawn(...)
      job.on('succeeded', ()=>console.log('succeeded'))
    • In addition, property job.timer returns the response of setTimeout in case option after was used, or setInterval in case of option every. For example, this allows to stop a regular running job like that:
      let job = cds.spawn({ every:111 }, ...)
      await sleep (11111)
      clearInterval (job.timer) // stops the background job loop

    The implementation guarantees decoupled execution from request-handling threads/continuations, by…

    • constructing a new root transaction tx per run using cds.tx()
    • setting that as the background run’s continuation’s cds.context
    • invokes fn passing tx as argument to it.

    Think of it as if each run happens in an own thread with own context, with automatic transaction management.

    Use argument options if you want to run the background thread with different user or tenant than the one you called cds.spawn() from.

    cds/srv.tx (…) → tx<srv>

    srv.tx (context?, fn?) → tx<srv>

    Use srv.tx() to start new app-controlled transactions manually, most commonly for database services as in this example:

    let db = await'db')
    let tx = db.tx()
    try {
      await (SELECT.from(Foo))
      await tx.create (Foo, {...})
      await (Foo)
      await tx.commit()
    } catch(e) {
      await tx.rollback(e)


    • context – an optional context object → see below
    • fn – an optional function to run → see below

    Returns: a transaction object, which is constructed as a derivate of srv like that:

    tx = Object.create (srv, Object.getOwnPropertyDescriptors({

    In effect, tx objects …

    • are concrete context-specific — i.e. tenant-specific — incarnations of srves
    • support all the Service API methods like run, create and read
    • support methods tx.commit and tx.rollback as documented below.

    Important: The caller of srv.tx() is responsible to commit or rollback the transaction, otherwise the transaction would never be finalized and respective physical driver connections never be released / returned to pools.

    srv.tx ({ tenant?, user?, … }) → tx<srv>

    Optionally specify an object with event context properties as the first argument to execute subsequent operations with different tenant or user context:

    let tx = db.tx ({ tenant:'t1' user:'u2' })

    The argument is an object with these properties:

    • user — a unique user ID string or an instance of cds.User
    • tenant — a unique string identifying the tenant
    • locale — a locale string in format <language>_<region>

    The implementation constructs a new instance of cds.EventContext from the given properties, which is assigned to tx.context of the new transaction.

    Learn more in section Continuations & Contexts.

    srv.tx ((tx)=>{…}) → tx<srv>

    Optionally specify a function as the last argument to have commit and rollback called automatically. For example, the following snippets are equivalent:

    await db.tx (async tx => {
      await (SELECT.from(Foo))
      await tx.create (Foo, {...})
      await (Foo)
    let tx = db.tx()
    try {
      await (SELECT.from(Foo))
      await tx.create (Foo, {...})
      await (Foo)
      await tx.commit()
    } catch(e) {
      await tx.rollback(e)

    In addition to creating a new tx for the current service,

    srv.tx (ctx) → tx<srv>

    If the argument is an instance of cds.EventContext the constructed transaction will use this context as it’s tx.context. If the specified context was constructed for a transaction started with cds.tx(), the new transaction will be constructed as a nested transaction. If not, the new transaction will be constructed as a root transaction.

    cds.context = { tenant:'t1', user:'u2' }
    const tx = cds.tx (cds.context)
    //> tx is a new root transaction
    const tx = cds.context = cds.tx ({ tenant:'t1', user:'u2' })
    const tx1 = cds.tx (cds.context)
    //> tx1 is a new nested transaction to tx

    tx.context cds.EventContext

    Each new transaction created by cds.tx() will get a new instance of cds.EventContext constructed and assigned to this property. If there is a cds.context set in the current continuation, the newly constructed context object will inherit properties from that.

    Learn more in section Continuations & Contexts.

    tx.commit (res?) ⇢ res

    In case of database services, this sends a COMMIT (or ROLLBACK) command to the database and releases the physical connection, that is returns it to the connection pool. In addition, the commit is propagated to all nested transactions.

    The methods are bound to the tx instance, and the passed-in argument is returned, or rethrown in case of rollback, which allows them to be used as follows:

    let tx = cds.tx() .then (tx.commit, tx.rollback)

    tx.rollback (err?) ⇢ err

    In case of database services, this sends ROLLBACK command to the database and releases the physical connection. In addition, the rollback is propagated to all nested transactions, and if an err object is passed, it is rethrown.

    See documentation for commit for common details.

    Note: commit and rollback both release the physical connection. This means subsequent attempts to send queries via this tx will fail.


    srv.tx (req) → tx<srv>

    Prior to release 5, you always had to write application code like that to ensure context propagation and correctly managed transactions:

    this.on('READ','Books', req => {
      const tx = cds.tx(req)
      return ('Books')

    This still works but is not required nor recommended anymore.