Designing Cloud-Native Extensions on SAP BTP with CAPM: Patterns for Clean-Core Architectures

Wait 5 sec.

Key Takeaways●  Clean core extensibility is when we, as a design choice, decide to go that way per use case: synchronous OData for transactional use cases that require immediate response, asynchronous events for all other use cases.●   At the beginning of each CAPM project, multi-tenancy issues should be addressed; retrofitting tenant isolation in a single-tenant already deployed extension is a much more disruptive affair than to include the feature from the word go.●   Principal propagation does not feature in the design of which extensions preserve core system authorization and audit trails’ integrity versus which ones allow them to degrade silently. ●  In CAPM extensions the great production risk is not in the framework at all but in the peripheral issues: destination config, XSUAA scope mapping, CDS to core field mapping which go out of sync across environments.●  Clean core, which reduces upgrade risk in the ERP core but does not remove operational burden; it shifts versioning, monitoring, and authorization complexity into the BTP environment, which in turn requires the same operational care as the core itself.Executive SummarySAP’s clean-core strategy tells engineering teams to leave S/4HANA itself largely untouched. Custom logic should run beside it as applications on SAP Business Technology Platform, usually built with the Cloud Application Programming Model (CAPM). That change is not merely structural. Teams now have to deal with distributed-system problems—network requests instead of local function calls, tenant isolation, token-based authorization, and eventual consistency—in a domain long shaped by monolithic customization. This article examines recurring design choices in production CAPM extension projects, including how extensions compose services with the core, how tenants are separated, how SAP Event Mesh carries integration events, and how authorization relies on principal propagation. The discussion comes from hands-on experience designing and building an extension from start to finish, with architects and senior engineers in mind—people who need to make these tradeoffs correctly on the first attempt.IntroductionFor most of the past two decades, extending SAP meant changing the ERP system itself. Teams added ABAP reports, user exits, and Z-tables alongside the vendor’s standard code. The approach worked, but the trade-off was familiar: each custom object could fail during the next upgrade, and the line separating SAP’s code from the company’s own gradually disappeared.Keep the ERP core to stock standards, and put custom logic into a separate cloud native runtime, SAP BTP, which can be stood up or taken down independent of the core’s release cycle [1]. CAPM is what SAP uses for that outer layer: a domain model-first, convention over configuration approach which compiles Core Data Services (CDS) models into OData services, with persistence, authorization and multitenancy mainly handled by generated code [2].The problem is simple: “move the logic to BTP” is a slogan, not an architecture. Accepting it leaves a team to settle the details that improvised projects often mishandle: connecting the extension to the core, serving several business units from one codebase without exposing their data to one another, carrying a user’s identity across the network, and monitoring a service beyond the familiar SAP Basis toolchain. Without a deliberate design, teams tend to rely on synchronous calls, technical users rather than propagated authorization, and single-tenant deployments. The second consumer then arrives, and the design has to be rebuilt. These are the same distributed-systems errors organizations make during a first microservices project—only they reach the SAP ecosystem later.Service design, multi-tenancy, event based integration, and authorization of principals we also look at what goes wrong in production. This article looks at the most important issues: BackgroundStandard configuration and key-user (in-app) extensibility is what we have in S/4HANA; for anything that requires custom code or a new data model we go to side by side extensions on BTP [1], [5]. The RESTful ABAP Programming Model (RAP) is the clean core compatible way to expose logic that must stay in ABAP, which we build on CDS views and which generates OData services with transactional (draft, lock, validation) handling [5], [9].In the world of side-by-side half we have CAPM which is the model-driven framework. A CAPM project puts forth entities and services in CDS; the framework in turn puts out an OData v4 (or v2) service with default persistence and generic CRUD handlers which also has in it hooks for custom logic in Node.js or Java [2], [4]. Also it is typical for CAPM apps to run on Cloud Foundry or Kyma, which in turn is backed by SAP HANA Cloud, and we also see Fiori elements UIs generated from the same CDS annotations.SAP Event Mesh is the managed version of the event-broker pattern which we have in BTP middleware that puts together event delivery between producers and subscribers so their availability and load are put out of each other’s business. It is the typical means for asynchronous integration between the core (which is publishing business events) and extensions that may react without polling.XSUAA, the SAP Authorization and Trust Management service, is BTP’s OAuth2 auth server: it issues and issues JWTs which have in them scopes from role collections and is what CAPM apps use for auth and auth requests [6], [7].Problem AnalysisFour problems recur across CAPM extension projects, independent of industry:Where does the transaction boundary sit? A parallel extension is not in the same unit of work as the S/4HANA tables it depends on. What used to be a single local transaction now requires a synchronous call with its own error handling, or an asynchronous flow which includes eventual consistency. Teams that ignore this design step implement synchronous calls into every operation which in turn caps the extension’s availability to that of the core system.How many tenants does the extension actually serve? In many cases, a single business unit’s extension is also required by a second unit or country within the first year. If tenant isolation was not a primary design issue from the start, then as we go in to retrofit it we are looking at re-provisioning infrastructure and re-testing authorization, which at this point the first tenant is already in production. Whose identity is on the wire? Calling S/4HANA through a broad-privilege technical user is easier than setting up identity federation, which explains its popularity. But it creates a serious authorization flaw: everyone shares one credential, so individual checks disappear and the audit trail points to the technical account rather than the person who made the change.Who is watching the extension once it is live? A CAP service does not inherit the monitoring practices that come with the ABAP stack; a Basis team’s muscle memory for ST22 reports does not translate to Cloud Foundry logs, which in turn must be deliberately wired into an observability pipeline. Architecture and DesignIn the case of S/4HANA, we maintain minimal custom logic (standard objects, ABAP CDS views, RAP exposed business objects), which in turn has a side-by-side CAPM extension in a BTP subaccount for UI, extended logic, and any data model that does not fit in the ERP schema.Three decisions define this shape. Service composition: The extension connects to the core in two ways. Requests that need an immediate response use OData v2/v4 through the Destination and Connectivity services. Notifications go through SAP Event Mesh instead. In practice, asynchronous communication should be the default; use synchronous calls only when the caller cannot continue without the result, because they tie the extension’s SLA to a system outside its control.Multitenancy: CAPM's @sap/cds-mtxs package implements SAP's standard pattern — one provider subaccount running the extension, an MTX sidecar handling tenant onboarding/upgrade, and one isolated database container per tenant subaccount [3], [10], [12]. This we see a shared application, isolated data model which is more economical than one deployment per tenant and we keep tenant data separate which is a point of note for the compliance reviewers.Authorization boundary: XSUAA-issued JWTs authorize the extension's own endpoints; the Destination service, configured for principal propagation, carries the caller's identity through to the core so OData calls execute — and are authorization-checked and logged — as the actual end user [6], [7], [8].The tradeoff: this trades in-core simplicity for operational surface area — a second deployable, security model, and monitoring domain, in exchange for upgrade independence. That trade is worth making for most net-new logic; it is not automatically worth making for a small, stable customization cheaper to maintain in place, a point returned to below.ImplementationThe following snippets illustrate the patterns above at the level of real project code.Domain model keyed against a core identifier, not a replica of core data:// db/schema.cds namespace com.example.extension; using { cuid, managed } from '@sap/cds/common';  entity RiskAssessments : cuid, managed {   salesOrder   : String(10) @title: 'S/4HANA Sales Order';   riskScore: Decimal(3,0);   reviewStatus : String(20) default 'OPEN';   reviewedBy   : String(256);   notes    : String(1000); }The entity carries a plain salesOrder string rather than an association into a replicated copy of the order table — replicating core master data reintroduces exactly the coupling clean core is meant to remove. The extension resolves the current state on demand instead.Synchronous read-through to the core via a remote OData service:// srv/external/S4SalesOrder.cds using { API_SALES_ORDER_SRV as external } from './S4SalesOrder.csn'; service S4Service {   entity SalesOrders as projection on external.A_SalesOrder; }// srv/risk-service.js const cds = require('@sap/cds'); module.exports = cds.service.impl(async function () {   const { RiskAssessments } = this.entities;   this.on('READ', RiskAssessments, async (req, next) => { const results = await next(); const s4 = await cds.connect.to('S4Service'); for (const row of results) {   const order = await s4.run(         SELECT.one.from('SalesOrders').where({ SalesOrder: row.salesOrder })   );   row.orderStatus = order?.OverallSDProcessStatus; } return results;   }); });cds.connect.to('S4Service') resolves, via configuration, to a destination that carries the inbound user's principal — propagation is configuration, not application code, so the same handler runs correctly whether the destination uses principal propagation or, in a lower environment, a technical user.Reacting to a core event asynchronously, instead of polling:// srv/event-listener.js const cds = require('@sap/cds'); module.exports = cds.service.impl(async function () {   const messaging = await cds.connect.to('messaging');   messaging.on('sap.s4.beh.salesorder.v1.SalesOrder.Changed.v1', async (msg) => { const { SalesOrder } = msg.data; await UPDATE(cds.entities.RiskAssessments)   .set({ reviewStatus: 'NEEDS_REVIEW' })   .where({ salesOrder: SalesOrder });   }); });This handler updates the extension's own state and lets a downstream process act on it — decoupling that keeps the extension available even during a core maintenance window.Authorization declared next to the model it protects:// srv/risk-service.cds service RiskService @(requires: 'authenticated-user') {   entity RiskAssessments as projection on extension.RiskAssessments;   annotate RiskAssessments with @( restrict: [   { grant: ['READ'], to: 'RiskViewer' },   { grant: ['*'],to: 'RiskManager' } ]   ); }RiskViewer/RiskManager map, via xs-security.json, to XSUAA scopes and role collections assigned to real users:{   "scopes": [ { "name": "$XSAPPNAME.RiskViewer", "description": "Read risk assessments" }, { "name": "$XSAPPNAME.RiskManager", "description": "Manage risk assessments" }   ],   "role-templates": [ { "name": "RiskViewer", "scope-references": ["$XSAPPNAME.RiskViewer"] }, { "name": "RiskManager", "scope-references": ["$XSAPPNAME.RiskManager"] }   ] }These role templates are what an administrator turns into actual role collections and assigns to users or groups in the BTP cockpit — CDS declares intent, this file is the contract USAA enforces against, and the role collection assignment is the step most likely to drift out of sync with the other two, as the Production Lessons Learned section below discusses.Performance and ScalabilityCAPM applications scale the way most stateless Cloud Foundry/Kyma services do: horizontally, with session state kept out of the application tier. The design questions that matter are elsewhere. Read-through calls to the core are the dominant latency cost, not the extension's own logic — a short-lived cache for infrequently changing master data reduces both, with a TTL reflecting how stale the UI may be for that field. One container per tenant which reduces cross-tenant contention but at the same time does require capacity planning against the largest tenant’s needs. Event consumption throughput is a function of the consumer which in turn means that for bursty events there must be sufficient number of consumer instances to handle the volume within the business’s freshness requirement. For bulk operations use OData’s $batch instead of individual calls in a loop which in turn results in one round trip as opposed to hundreds.Security ConsiderationsSecurity in this architecture is a matter of the full request path maintaining the same guarantees which in turn each individual element of that path is responsible for and not of any single control. ●  Token validation is automatic; scope design is not. At each point of entry we require a valid XSUAA JWT, also it is CAPM’s job to check signature, expiry, and audience which we do without custom code but still we have to write out xs-security.json scope definitions carefully because a very wide default scope renders CDS @restrict annotations useless [6].● Principal propagation should be the default backend authentication mode, Not an issue that is addressed only as an after thought [7], [8]; any technical or user fallback should be put in place as a documented reviewed exception, not a silent default.●  Authorization granularity matters more than authentication. OWASP’s API Security Top 10 puts out broken object level authorization and broken function level authorization as its top priority issues [14] which in turn map directly to CDS @restrict annotations that are too broad, or which are absent at all on a generated navigation property.●  Secrets live in the platform, not the codebase. Destination credentials and API keys go into BTP’s credential store and injected service bindings which also means they should not be in the CDS model or a checked in config file. ● Transport security needs attention at both ends. BTP by default enforces TLS 1.2+ on inbound traffic; for outbound connection via the Cloud Connector that is the team’s responsibility as far as certificate rotation goes, not the platform’s.●  Zero-trust framing fits this architecture naturally. NIST SP 800-207 puts forth zero trust access in terms of per session and identity which is a different approach from that of network location [19] we present this model which by design has the application logging and the core no longer on the same trusted network segment.Operational ExcellenceA CAP service is only as operable as the observability wired around it, and BTP does not wire that in for free.●  Structured, correlated logging. The Application Logging feature provides for search of logs across instances, but only if correlation IDs travel with each request through every downstream call which includes outbound OData calls to the core [24] without that a single reported issue from a user may not be traced across the full request path.●  Distributed tracing matters more here than in a typical microservices system, because a meaningful share of "slow" requests are slow in the core, not the extension, and only a trace spanning both sides of the boundary proves which one [21].●  Alerting should be scoped to actionable symptoms — We see that for elevated error rate, queue backlog, destination authentication failures not all anomalies the platform is technically able to emit; alert fatigue, which leads to practice defeat much faster than any one missed incident.●   Multitenant upgrade rollout needs a staged path. MTX-powered schema upgrades go out to all enrolled tenants at the same time by default, which is why we require a canary tenant (or a small set) for an upgrade to go through before it is made available to all.●  HA/DR is the team's own responsibility. For critical business extensions, we need to see a defined RTO/RPO and a proven out-of-band recovery method; living on a cloud platform does not, in and of itself, guarantee resilience.Trade-offs and Limitations     Clean core, side-by-side extension architecture is what we should go with for new custom logic implementation, but that is not a hard and fast rule. It introduces a network hit and an availability issue which core customizations did not have; in a use case which requires the same transactional consistency as native ABAP logic, forcing it side by side actually trades off upgrade risk for distributed transaction complexity which may not be a clear win. Also, it is not a free ride; each extension is a second thing to secure, patch, and staff. Multi-tenancy issues are a waste of effort for a tool which is very scoped and truly single-tenant, and event-driven integration trades off speed for resilience, which is the wrong trade-off when the business is looking for a sync answer. Small, stable, rarely used custom codes are often better left as minimal in core extensions, then re-platformed for strategic conformity -- clean core is a direction to move logic towards over time, not a reason to do a big bang migration of everything at once.Production Lessons LearnedIn many cases we saw two failure patterns which reported back often enough in real CAPM extension builds to be worth going into in detail; we thought at first they were application bugs in either of these and in fact they were not.The "it works in every environment except the one that matters" destination bug. A CAP service may read from S/4HANA without trouble in development and quality assurance, then fail silently—or return a misleading error—when it reaches production. The application code is often fine. More commonly, production has a destination with the wrong authentication setup: basic authentication was configured instead of principal propagation, or a certificate that works in QA was never replaced with the production certificate. Development and QA tests still pass because each one uses its own correctly configured destination. The problem appears only in the environment that no one tests again before go-live. A lasting fix means managing destination configuration like versioned application code. Each change needs review, and every promotion should include a checklist confirming the authentication method and certificate validity for each landscape—not a cockpit setting configured once and then forgotten.The silent authorization drift between the CDS model and the role collections that were actually assigned. A CDS @restrict is a statement of intent which the actual behavior a user sees is based on the role collection which an admin put together in the identity provider and which two which are edited in different tools by different people at different times. Early in an extension's life the mapping between CDS scopes and role collections is usually correct as one team designed both at the same time. Months down the line after a business request for a new field or operation which a developer then adds to the @restrict annotation which he extends the CDS scope for -- the corresponding role collection update is easy to miss what breaks is not immediate but the new feature just does not show up for anyone which is reported as a "bug in the new feature" instead of being identified as an authorization gap. The best fix is a single mapping document which is owned by someone and reviewed with the security team on a set cadence not just at go live so that a CDS change and its corresponding role collection change are always done together instead of independently.An issue we see is that what began as a solution for one team grew to require a second and by that point the migration was much more difficult we had wished we had put tenant awareness in from the beginning. Also we note that Fiori elements really do speed up UI delivery when the backend CDS @UI annotations are used as designed teams which ignore this and go to manual UI coding see no benefit at all out of the framework which they may have thought would be the solution for them.Best Practices●  Default to async, event based integration for notifications; use sync OData for interactions that require an immediate response. ●  Multitenancy model to be determined before writing the first line of extension code, which in turn may be for a single tenant launch.●  Principal propagation to be the default back-end auth mode out of which technical user fall back will be a documented exception.●   Write out CDS @restrict annotations for what the security team has approved in terms of role collection design, and at a set interval go over that mapping. ●  For destination and connectivity config, version and review the same care that we put into application code. ●   Put in place distributed tracing that spans the extension-to-core boundary from the very first production deployment, not only after the first latency issue.●    Cache core master data reads with an explicit, business-justified TTL.●   Treat HA/DR posture as an explicit requirement with a stated RTO/RPO, sized to the process's business criticality.Future OutlookSAP reports that it is presenting RAP (which is for logic that must stay in ABAP Cloud) and CAPM (which is for logic that belongs side by side) as embodiments of the same clean-core philosophy which we see in the use of CDS as the model language. As cross-system communication trends towards event-based models, extensions which are put together in a synchronous-first approach will see growth in the need to include asynchronous elements. Also, these trends do not change the base principles on which we operate; they, in fact, put into higher value the issues of service composition and multitenancy from the start.ConclusionIn a side-by-side layer, a team owns end-to-end, not scattered through a vendor-maintained core. CAPM makes that layer a material improvement over hand-rolled middleware, but it does not do away with the underlying distributed systems issues: service composition, tenant isolation, identity propagation, and observability still have to be designed into the layer. Teams which approach those design issues with the same care as any other production distributed system do in fact end up with extensions that are truly upgrade independent; teams which see CAPM as a drag-and-drop solution tend to learn the hard way that these issues did not go away.References[1] SAP SE, "What Is a Clean Core?" SAP.com, 2026. https://www.sap.com/resources/what-is-a-clean-core[2] SAP SE, "Getting Started," capire — SAP Cloud Application Programming Model documentation. https://cap.cloud.sap/docs/get-started/[3] SAP SE, "Deploy Multitenant SaaS Applications," capire. https://cap.cloud.sap/docs/guides/multitenancy/[4] SAP SE, "Bootstrapping Servers," capire — CAP Node.js documentation. https://cap.cloud.sap/docs/node.js/cds-server[5] SAP SE, "ABAP RESTful Application Programming Model," SAP BTP documentation (SAP-docs mirror of SAP Help Portal). https://github.com/SAP-docs/btp-cloud-platform/blob/main/docs/30-development/abap-restful-application-programming-model-33a301e.md[6] SAP SE, "What Is the SAP Authorization and Trust Management Service?" SAP BTP documentation (SAP-docs mirror). https://github.com/SAP-docs/btp-cloud-platform/blob/main/docs/60-security/what-is-the-sap-authorization-and-trust-management-service-649961f.md[7] SAP SE, "Configure Principal Propagation via OIDC Token," SAP BTP Connectivity documentation (SAP-docs mirror). https://github.com/SAP-docs/btp-connectivity/blob/main/docs/1-connectivity-documentation/configure-principal-propagation-via-oidc-token-000232b.md[8] SAP SE, "Security," SAP BTP documentation (SAP-docs mirror). https://github.com/SAP-docs/btp-cloud-platform/blob/main/docs/60-security/security-e129aa2.md[9] SAP SE, "Security Concepts," SAP BTP Best Practices Guide (SAP-docs mirror). https://github.com/SAP-docs/btp-best-practices-guide/blob/main/docs/set-up-and-plan/security-concepts-951d36c.md[10] SAP SE, "Multitenant SaaS Applications on SAP BTP Using CAP — Tried and True," SAP Community, Oct. 2022. https://blogs.sap.com/2022/10/19/multitenant-saas-applications-on-sap-btp-using-cap-tried-and-true/[11] SAP SE, "Principal Propagation in SAP Integration Suite," SAP Community. https://community.sap.com/t5/technology-blog-posts-by-sap/principal-propagation-in-sap-integration-suite/ba-p/13528717[12] SAP Samples, "Understand BTP Multitenancy," btp-cap-multitenant-saas repository. https://github.com/SAP-samples/btp-cap-multitenant-saas/blob/main/docu/1-discover/5-understand-btp-multitenancy/README.md[13] OASIS, "OData Version 4.01. Part 1: Protocol," OASIS Standard, Apr. 2020. https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html[14] OWASP Foundation, "OWASP API Security Top 10 — 2023," 2023. https://owasp.org/API-Security/editions/2023/en/0x11-t10/[15] D. Hardt, Ed., "The OAuth 2.0 Authorization Framework," IETF RFC 6749, Oct. 2012. https://www.rfc-editor.org/rfc/rfc6749.html[16] M. Jones, J. Bradley, and N. Sakimura, "JSON Web Token (JWT)," IETF RFC 7519, May 2015. https://www.rfc-editor.org/rfc/rfc7519.html[17] Cloud Native Computing Foundation, "CNCF Cloud Native Definition v1.1," CNCF Technical Oversight Committee. https://github.com/cncf/toc/blob/main/DEFINITION.md[18] R. Chandramouli, "Security Strategies for Microservices-based Application Systems," NIST Special Publication 800-204, Aug. 2019. https://csrc.nist.gov/pubs/sp/800/204/final[19] S. Rose, O. Borchert, S. Mitchell, and S. Connelly, "Zero Trust Architecture," NIST Special Publication 800-207, Aug. 2020. https://tsapps.nist.gov/publication/get_pdf.cfm?pub_id=930420[20] R. Chandramouli and Z. Butcher, "Building Secure Microservices-based Applications Using Service-Mesh Architecture," NIST Special Publication 800-204A, May 2020. https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-204A.pdf[21] OpenTelemetry Authors, "OpenTelemetry," Cloud Native Computing Foundation, 2026. https://opentelemetry.io/[22] SAP SE, "Use the OData v4 Type-Safe Client API," SAP Cloud SDK for Java documentation. https://sap.github.io/cloud-sdk/docs/java/features/odata/v4-vdm-client[23] SAP SE, "What Are Event Brokers?" SAP Integration Suite documentation (SAP-docs mirror). https://github.com/SAP-docs/btp-integration-suite/blob/main/docs/ISuite_Event_Mesh/what-are-event-brokers-f72428f.md[24] SAP Community, "Consuming Business Events Using SAP Integration Suite, Advanced Event Mesh," SAP Community (official SAP blog). https://community.sap.com/t5/technology-blog-posts-by-sap/consuming-business-events-using-sap-integration-suite-advanced-event-mesh/ba-p/13559412Author BioThe author is a senior level SAP engineering consultant which has for 20 plus years’ experience in enterprise SAP development across the US, Singapore, Hong Kong, and the Middle East, we also find that he is a specialist in SAP Fiori, SAPUI5, OData, ABAP, ABAP CDS, the RESTful ABAP Programming Model and cloud native extension architecture on SAP Business Technology Platform which he works with via the Cloud Application Programming Model. Also he has SAP Certified Development Associate credentials in SAP Fiori Application Development and SAP Cloud Platform, an SAP Certified Technology Associate in SAP Fiori System Administration, an SAP Certified Application Associate in SAP Analytics Cloud and is a Certified SAFe 5 Advanced Scrum Master. Also included in his work is the design and development of side by side BTP extensions as part of clean core initiatives on large scale enterprise projects which also included service composition, OData based integration with S/4HANA, and authorization and role design.