Every time this question comes up on a new BTP project, it gets framed as RAP versus CAP, like you're picking a winner between two frameworks competing for the same job. They're not competing for the same job. RAP and CAP solve overlapping problems from opposite sides of a system boundary, and the question that actually decides which one you reach for isn't which is technically better, it's where your data already lives and who's going to be maintaining this thing in three years.
Where the data lives decides more than the language does
If ninety percent of the entity model you're building is S/4 tables and CDS views you're extending, RAP keeps you inside that system, reading and writing against the data with native access to the standard business object's locking, number ranges, and authorization concepts, without a network hop in either direction. You're not calling out to S/4 from somewhere else, you're sitting next to the data you're working with.
The moment your app needs to pull from multiple systems in the same business process, S/4 plus a non-SAP database, plus a third party API, plus maybe a SuccessFactors or Ariba call, RAP stops being a natural fit, because RAP's whole value proposition assumes you're close to one system's data model. That's exactly the situation CAP was built for. It runs as its own deployable unit on BTP, side by side with everything it talks to, with no inherent bias toward any one of those systems.
Your team's actual skillset, not the one you wish they had
A team of classic ABAP developers picking up RAP is a real learning curve, managed versus unmanaged implementations, draft handling, behavior definitions, but it's still ABAP syntax and still inside the mental model of business objects they already understand from years of working with BAPIs and standard tables. It's a steep hill, not a cliff.
CAP means an actual language switch, to Node.js or Java, plus a different dependency and deployment model entirely, npm instead of transports, CI/CD pipelines instead of the standard transport chain. CDS modeling concepts carry over, which helps, but the runtime doesn't, and a team of classic ABAP developers handed a CAP project without real ramp time tends to write CAP services that read like ABAP translated word for word into JavaScript: procedural, defensive, fighting the framework instead of using it. I've seen that firsthand, and it's not a fast thing to unlearn once the bad habits are baked into a service that's already in production.
Lifecycle coupling: riding the upgrade train or not
RAP delivered through embedded development rides inside the S/4 system's own lifecycle. Your custom code goes through the same transport chain, the same upgrade windows, the same authorization model your Basis team already manages. That's a feature if your release cadence is fine being tied to the S/4 system's, and a real constraint if it isn't.
CAP on BTP is its own deployable artifact with its own versioning and its own scaling, completely decoupled from whenever the S/4 system next goes through an upgrade. If the business wants to ship changes to this app weekly while the core S/4 system upgrades twice a year, that decoupling isn't a nice to have, it's the entire reason the project should be on CAP in the first place.
Clean core didn't make this decision easier, it made it explicit
SAP's clean core guidance pushes net-new business capability toward side by side extensions on BTP, generally CAP, and reserves in-app extensibility, RAP through key user or developer extensibility, for things that genuinely belong inside the S/4 transaction boundary: field level extensions, validations and determinations on existing business objects, custom Fiori apps surfacing data that already lives there. That guidance doesn't remove the judgment call, it just gives you a default to argue against when a specific project has a reason to deviate from it.
A custom approval workflow object, owned by its own table, with maybe one or two lookups back into S/4 and nothing else, is a strong CAP candidate even when the team's instinct is to build it in RAP because that's the muscle memory everyone already has. Conversely, a custom validation and a couple of computed fields bolted onto the Sales Order header, something that needs to participate in the standard SO's lock object, draft handling, and number range logic the moment it touches that object, doing that outside RAP means re-implementing concurrency and locking from scratch against a system that already solved it twenty years ago. That's not a CAP project no matter how comfortable the team is with Node.
What this actually looks like on a real project
Most architectures that get this right don't pick one framework for the whole landscape. RAP exposes OData on the S/4 side for the data that genuinely lives there, and a CAP service on BTP orchestrates across that plus whatever else the process needs, presented through a single Fiori launchpad so the end user never knows or cares which backend answered which call. Treat it as a decision made per bounded context, not a single answer you commit to for the whole landscape on day one. The question to ask for each piece isn't "RAP or CAP," it's "where does this specific data actually live, and what does this specific piece need to do that the other system doesn't already do well."