Side-by-Side Without Relocating the Debt: Governance Patterns for CAPM (Series 3, Part 4: Clean Core + AI)
By Deepa Dorairaj | SAP AI Solution Architect | Series 3, Part 4 of 6 | Published September 2026
TL;DR: Clean Core done right increases agility. Clean Core done under deadline pressure just moves the mess into CAPM. This part gives you the actual mechanics to prevent that: how to consolidate at the service boundary instead of porting 1:1, real quality-gate tooling for CAP (not just "write tests"), an authorization governance pattern, and a debt register format you can actually maintain.
Series Context
Part 3 covered what SAP's Custom Code Migration Agent automates and where it explicitly hands the decision back to a human. This part is for that handback moment — what to actually do with an object once a human has to decide.
SECTION 1 — THE FAILURE MODE, IN CODE
Here's what a 1:1 port looks like, concretely. Say you have a Z-program handling vendor evaluation scoring — three separate custom transactions, each with its own hardcoded logic, each calling the same underlying vendor master data slightly differently. The deadline-pressure move is to spin up three separate CAP services, one per transaction, each replicating the original Z-program's structure almost exactly:
service VendorScoreServiceA {
action calculateScoreA(vendor: String, region: String) returns Decimal;
}
service VendorScoreServiceB {
action calculateScoreB(vendorId: String, plant: String) returns Decimal;
}
service VendorScoreServiceC {
function getScoreC(vendor: String) returns Decimal;
}Three services, three parameter conventions (vendor vs vendorId), two different verbs for functionally identical operations (action vs function), and the same underlying business logic duplicated three times. This is technical debt with a CAP file extension. It compiles, it deploys, it demos fine. It's exactly as fragile as the three original Z-programs, just relocated.
SECTION 2 — CONSOLIDATING AT THE SERVICE BOUNDARY
The alternative requires actually modeling the domain, not translating the transaction list. A consolidated version:
using { VendorMaster } from './vendor-master';
service VendorEvaluationService {
entity VendorScores as projection on VendorScore;
@readonly
entity VendorScore : cuid, managed {
vendor : Association to VendorMaster;
region : String(3);
plant : String(4);
score : Decimal(5,2);
scoredAt : Timestamp;
method : String enum { standard; regional; plantSpecific };
}
function calculateScore(vendor: VendorMaster:ID, method: String) returns Decimal;
}One entity model, one consistent parameter convention, one calculation function that branches internally on method instead of existing as three separate services. The three original Z-programs' distinct business logic doesn't disappear — it becomes three code paths inside one well-modeled function, documented and testable as variations of one concept instead of three unrelated services.
This is the actual work Part 1 called "decision cost" — someone has to sit down, look at all three Z-programs, and decide they're really one domain concept with three scoring methods, not three separate features. That's a modeling decision, not a migration task, and it's the step a rushed timeline skips.
SECTION 3 — QUALITY GATES: THE CAPM EQUIVALENT OF ATC
Part 2 covered ATC as ABAP's quality gate. CAPM needs its own, and it exists — it's just less commonly wired into CI/CD than it should be:
@SAP/eslint-plugin-cds— static analysis for CDS models specifically, catching naming inconsistencies, unused elements, and structural issues before they ship. Run it ascds lintin your build pipeline, not just as an editor plugin.cds compilein CI, not just locally — a compile step in your pipeline catches broken model references immediately. This is the "compiler in the loop" principle from a community post I covered a few weeks back: an AI-assisted or human-written CDS change that compiles clean in isolation can still break silently against the wider model. Runningcds compile --to sqlandcds compile --to edmxas explicit CI steps, not just relying on local dev testing, catches that before it merges.cds testfor actual behavioral coverage — CAP's own testing framework lets you spin up an in-memory version of your service and run real requests against it. For the consolidatedVendorEvaluationServiceabove, that means a test permethodbranch, not just a smoke test that the service starts.cds-caching(the open-source plugin covered in an earlier post) is worth mentioning here specifically for a different reason: retrofitting caching onto a badly consolidated service is much harder than adding it to a well-modeled one, because a clean entity model gives you clear invalidation boundaries. A tangle of three overlapping services doesn't.
SECTION 4 — AUTHORIZATION AS A GOVERNANCE ARTIFACT, NOT AN AFTERTHOUGHT
ABAP authorization objects have decades of institutional discipline behind them. CAP's equivalent — @requires and @restrict annotations tied to XSUAA scopes — is younger, and it shows in how often it gets bolted on late:
service VendorEvaluationService @(requires: 'authenticated-user') {
@restrict: [
{ grant: 'READ', to: 'VendorViewer' },
{ grant: ['READ','WRITE'], to: 'VendorAdmin' }
]
entity VendorScores as projection on VendorScore;
}The governance point: define these roles at the same time you consolidate the service, not after. A service migrated under deadline pressure often inherits an overly broad authenticated-user grant on everything, because scoping it properly takes a conversation about who actually needs write access — the same decision-cost problem again, just in the authorization layer instead of the data model.
SECTION 5 — A DEBT REGISTER YOU'LL ACTUALLY MAINTAIN
Part 1 argued that a debt register only works if it's honest and low-friction. The simplest version that survives contact with a real project is a CDS entity, versioned in your own repo, queryable like anything else:
entity TechDebtLog : cuid, managed {
service : String;
objectRef : String;
reason : String;
originalOwner : String;
reviewBy : Date;
status : String enum { open; reviewed; resolved };
}When a rushed consolidation ships with a known compromise — say, VendorScoreServiceC's logic got merged in without full test coverage on the plantSpecific path — log it here, with a real review date, not a TODO comment buried in code nobody searches for. Query it in a dashboard. Review it at a fixed cadence. It survives exactly as long as someone actually looks at it, so wire that review into an existing recurring meeting rather than inventing a new one nobody attends.
SECTION 6 — WHAT'S NEXT
Part 5 applies Sonja Liénard's three-concentric-circles model — ABAP Cloud core, ABAP AI tooling, agents — to a full worked example, showing how the consolidated service from this part fits into that larger architecture.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.