34 · Extensibility Patterns

Implement customer-exit hooks, extend entities at runtime, use cds-plugin.js, and @Core.SideEffects.

Episode 34 — Service Extensibility Patterns

What you'll learn

Why this matters

SAP-style platforms must be extensible — customers need to add fields, override business rules, and integrate without forking the base. These patterns are how enterprise CAP applications support that model cleanly.

Plugin hook pattern

// Base service — exposes hook points
module.exports = class BaseOrderService extends cds.ApplicationService {
  async init () {
    // Register hook slots other plugins can fill
    this.before('CREATE', 'Orders', this._beforeCreate.bind(this))
    this.on('CREATE', 'Orders', this._onCreate.bind(this))

    await super.init()
  }

  // Override these in subclass or plugin
  async _beforeCreate (req) { /* default: no-op */ }
  async _onCreate (req) {
    // ... default create logic
    await INSERT.into('Orders').entries(req.data)
    return SELECT.one.from('Orders').where({ ID: req.data.ID })
  }
}
// Extended service (customer or partner)
const BaseOrderService = require('@company/base-orders')

module.exports = class CustomOrderService extends BaseOrderService {
  async _beforeCreate (req) {
    // Inject custom validation without touching base code
    if (!req.data.costCenter) return req.reject(400, 'Cost center required')
  }
}

Customer-exit pattern (event-based)

// Base service emits an extensibility event
this.before('CREATE', 'Orders', async (req) => {
  // Allow external plugins to veto or enrich
  await this.emit('BEFORE_CREATE_ORDERS', req.data)
})

// Customer plugin subscribes
const baseService = await cds.connect.to('OrderService')
baseService.before('BEFORE_CREATE_ORDERS', (req) => {
  req.data.customField = computeCustomValue(req.data)
})

Extension fields

// Base model (not to be modified)
// db/base/schema.cds
namespace base;
entity Orders {
  key ID     : UUID;
  status     : String;
  total      : Decimal;
}
// Extension (in customer namespace)
// db/extensions/customer.cds
using base from '../../base/schema';

extend base.Orders with {
  costCenter   : String(10);
  projectCode  : String(20);
  approvedBy   : String;
}
// Also extend the service projection
using OrderService as svc from '../../srv/order-service';
extend projection svc.Orders with {
  costCenter,
  projectCode,
  approvedBy
}

Side effects (@Core.SideEffects)

When changing one field triggers recalculation of another in Fiori:

annotate svc.OrderItems with @(
  Core.SideEffects #quantityChanges: {
    SourceProperties: [quantity, price],
    TargetProperties: ['_it/total', '_it/taxAmount']
  }
);

This tells Fiori: when quantity or price changes, re-fetch total and taxAmount from the server.

Reusable CAP plugin npm package

my-cap-plugin/
├── package.json
├── index.js            ← plugin entry point
└── cds-plugin.js       ← auto-loaded by CAP on install
// package.json
{
  "name": "@mycompany/cap-audit",
  "version": "1.0.0",
  "cds": {
    "plugin": true
  }
}
// cds-plugin.js — runs at CDS bootstrap
const cds = require('@sap/cds')

// Hook into every service globally
cds.on('serving', (service) => {
  service.after(['CREATE','UPDATE','DELETE'], '*', (data, req) => {
    // Global audit log — applied to every entity in every service
    logAudit({ entity: req.target.name, op: req.event, user: req.user.id })
  })
})

Install in any CAP project with npm install @mycompany/cap-audit — the plugin registers automatically.

Feature flags for extensions

const featureFlags = await cds.connect.to('feature-flags')

this.before('CREATE', 'Orders', async (req) => {
  const enabled = await featureFlags.evaluate('newValidationRule', req.tenant)
  if (enabled) {
    // Run new validation only for tenants with the flag enabled
    await runNewValidation(req.data)
  }
})

Extensibility checklist

Item Pattern
Add new fields extend entity in customer namespace
Override business rule Override _beforeCreate in subclass
Add cross-cutting logic cds-plugin.js with cds.on('serving', ...)
Customer-specific workflow Subscribe to domain events
Feature rollout Feature flag evaluated per tenant
UI extensions Separate annotation cds file via extend projection

Hands-on exercise

  1. Move your service's validation logic into an overridable _validate(req) method
  2. Create a subclass that overrides _validate with stricter rules
  3. Create a cds-plugin.js that logs every CREATE across all services
  4. Declare an extension field priority on Orders via extend entity

Checkpoint ✓

Sign in for progress tracking & Q&A →