SAP CAP Event Handler Interview Questions: Choose before, on, and after Without Breaking Request Semantics
Quick Overview
Practice SAP CAP Node.js handlers with an executed before/on/after trace, next delegation, rejection tests and the distinction between after and commit. Primary references, original worked assets and explicit evidence boundaries support focused interview practice.
SAP CAP event handler interview questions often hide a lifecycle mistake inside otherwise reasonable code. Use before to validate or prepare input, on to fulfill or deliberately intercept a request, and after to enrich its result. An after handler is not a notification that the database committed. Within a phase, registration order also does not imply a dependable sequence.
The examples use CAP's Node.js API. Official documentation supplies the semantics; an original local fixture ran Service.send with @sap/cds 10.2.2 on Node v26.9.0. No HTTP adapter, database or deployed SAP environment was tested. Use PracHub's multi-file API review question to rehearse tracing a request before you rename handlers or add error handling.

Ask whether this is a request or an event message
Start with the delivery contract. Is the caller awaiting one operation result, or is the service publishing an asynchronous event to listeners? The same broad vocabulary can conceal different dispatch behavior. This article's worked case is an approve request with a result, not a broadcast to multiple independent consumers.
Official fact: CAP's core services reference distinguishes sequential on interception for requests from concurrent event listeners. It also describes concurrent before and after handlers. Therefore “all handlers run in the order I registered them” is not an adequate model.
In the fictional Inventory service, approving a positive quantity should return an approved result with a display label. Zero or negative quantities should be rejected. A wrapper should observe the operation without accidentally replacing its implementation. Assign each requirement to a phase before writing the handler.
| Responsibility | Phase or mechanism | Reason |
|---|---|---|
| Reject a nonpositive quantity | before('approve') | The operation should not run with invalid input |
| Produce the approved result | on('approve') | This is the request's fulfillment |
| Wrap the registered implementation | on with await next() | Delegation keeps downstream fulfillment reachable |
| Add a display label | after('approve') | The result already exists |
| React to a committed transaction | Request lifecycle hook | Service after alone does not establish commit |
The table is a design interpretation of the documented lifecycle. It does not imply that every validation belongs in custom JavaScript. Prefer declarative model constraints where they express the requirement accurately.
Follow one successful request through the handlers
This is the central portion of the executed fixture. It creates an in-process service, registers the handlers, and sends an action-like request. The synthetic object has no persisted entity behind it.
const cds = require('@sap/cds');
const srv = new cds.Service('Inventory');
srv.before('approve', req => {
if (req.data.qty <= 0) req.reject(400, 'POSITIVE_QTY_REQUIRED');
});
srv.on('approve', async (req, next) => {
const result = await next();
return result;
});
srv.on('approve', req => ({
qty: req.data.qty,
approved: true
}));
srv.after('approve', result => {
result.label = 'Ready';
});
const result = await srv.send('approve', { qty: 2 });
The instrumented run recorded validate, wrapper-enter, implementation, wrapper-exit, then enrich. Its result was { qty: 2, approved: true, label: 'Ready' }. The checker asserts the entire sequence and result, so the explanation can be compared with observable dispatch behavior.
Notice where the wrapper resumes: after the delegated implementation returns, before the after phase enriches the result. An interviewer can ask whether wrapper code should see the final display label. In this arrangement, it does not. Move the responsibility only after deciding which stage's result the wrapper actually needs.
The empty wrapper above is for teaching delegation. Production instrumentation would need a clear purpose, error behavior and data-handling policy. Adding a wrapper to every request without those requirements increases complexity while proving very little about the service.
Decide whether an on handler replaces or delegates
A request on handler can stop the chain by returning a result without invoking next(). That can be correct when the handler intentionally overrides fulfillment. It can also suppress a generic handler that the developer expected CAP to run automatically afterward.
The second executed fixture registers an override followed by a fallback. The override returns { source: 'override' } without delegation. The fallback call count remains zero. The override deliberately suppresses the fallback.
Now consider a wrapper that calls next() but forgets to await or return its promise. The caller's result and error propagation can become detached from the work you intended to wrap. Write an explicit asynchronous control flow and test both success and rejection. A downstream log message does not prove that the caller received the result correctly.
If the requirement is “do this instead of the generic database operation,” name the replacement's responsibilities. You may now own persistence behavior, result shape and any checks previously provided by the generic path. If the requirement is “add an observation around the existing operation,” delegation is usually the more relevant question.
CAP's custom code guide provides the registration context. Keep Java and Node.js examples separate: similar phase names do not make language-specific APIs interchangeable.
Reproduce the race between two before handlers
Suppose one handler normalizes input and another validates the normalized value. Registering the normalizer first can look sufficient until it yields.
parallel.before('check', async req => {
await Promise.resolve();
req.data.normalized = true;
});
parallel.before('check', req => {
// This handler must not depend on the other handler finishing first.
observed = Boolean(req.data.normalized);
});
The executed trace was normalizer-start, validator-sees-false, normalizer-end. Both handlers completed before fulfillment, but the second read happened before normalization finished. That is a concrete counterexample to treating a phase as a serial list.
Repair the dependency by combining ordered steps within one handler or calling a shared normalization function before the dependent validation inside that handler. Keep independent checks separate only when they really are independent. “Make the function async” does not create a sequencing relationship between two registered listeners.
The same concern applies to result enrichment. Two after handlers that write the same property can race. Prefer independent fields or one explicitly ordered enrichment function. For after('each', ...), the reference permits synchronous handlers; do not assume an asynchronous callback will be awaited as if it were a normal result-level handler.
Test a deliberately delayed normalizer, rather than relying on timing from a fast happy-path run. The point is to prove the dependency is represented in the code, not to hope the current scheduler happens to preserve your preferred order.
Reject deliberately and check which phases stop
With quantity zero, the original fixture rejects in before. Its trace contains only validate; neither the implementation nor enrichment runs. This checks a request rejection boundary, not just the returned error text.
Official fact: CAP's events reference distinguishes req.reject, which throws a request error, from req.error, which collects errors for the framework to inspect at phase boundaries. Choose deliberately. Collecting several validation findings can be useful, but code following req.error in the same function has not automatically stopped.
An interview answer should say whether subsequent code can still execute locally, whether the next phase should run, and what the caller receives. Avoid catching every exception and returning { ok: false } unless that is explicitly the service's response contract. Otherwise a failure may be transformed into an apparently successful operation result.
Also keep result mutation separate from rebinding a local parameter. Setting result.label changes the returned object in this example. Assigning a new object only to the handler's local parameter is not a reliable way to replace the service result. Test the caller-visible object rather than inspecting a temporary variable inside the handler.
Explain why after is still inside the request lifecycle
The word after invites a misleading conclusion: the business action is finished, so it must be safe to notify another system. The actual question is what the handler runs after. Here it runs after fulfillment, before the transaction outcome is necessarily final.
The events reference places succeeded, failed and done after the framework-managed transaction ends. It identifies before('commit') as a place that can still veto the request. Work performed after success cannot retrospectively veto a committed change, and database work in those later callbacks needs its own appropriate transaction context.

Imagine after('approve') sends an external email, then commit fails. The email may already be delivered while the approval is absent from the database. Moving a callback to succeeded avoids sending before that commit, but does not by itself guarantee durable delivery if the process stops before sending. Those are different failure windows.
For a reliable integration, discuss a transactionally recorded intent and the platform-supported delivery mechanism you would choose. Verify the exact mechanism's guarantees separately. This in-process fixture has no database or message broker, so it cannot establish atomic delivery, redelivery behavior or exactly-once effects.
Make the test boundary part of the answer
The saved fixture checks four things: successful phase order and result, rejection before fulfillment, concurrent same-phase dependency failure, and an override suppressing its fallback. These checks are useful because each distinguishes a specific incorrect mental model.
They do not test protocol error formatting, entity authorization, generic persistence, transaction rollback, remote messaging or deployment configuration. Add those tests where the real service introduces those responsibilities. The SAP BTP connectivity article addresses destinations and trust across network hops; a working destination does not prove correct handler sequencing.
If an interviewer adds an HTTP endpoint, explain how you would send an invalid request through the actual adapter and assert status, body and persisted state. If they add a database write, verify a later failure rolls it back. If they add a notification, identify both the commit relationship and the delivery retry owner. Each extension requires new evidence.
Turn a handler review into four falsifiable claims
When reviewing a proposed repair, write down its expected observation before running it. For the successful request, the caller must receive the enriched object. For quantity zero, the implementation call count must remain zero. For the wrapper, the fallback must remain reachable only when delegation occurs. For the delayed normalizer, the repair must remove the cross-handler dependency rather than merely make the current run faster.
Then ask what each passing check leaves unknown. The successful object says nothing about OData serialization. An implementation call count of zero does not prove a database transaction rolled back, because this fixture never created one. A local event trace does not establish message-broker redelivery. Keeping these distinctions visible prevents a test receipt from acquiring claims it did not earn.
An interviewer can also ask you to reorder the independent handlers. If the design is correct only under one registration order, either the handlers are not independent or the selected phase does not represent the required dependency. Express the sequence within one operation and retain separate listeners for work that can safely overlap. That is a concrete repair criterion you can apply to unfamiliar CAP code.
Practice request reasoning with related questions
These five verified PracHub questions exercise transferable API review, workflow, handler and transaction skills. They are not a dedicated CAP question bank. Use their broader prompts to explain the exact CAP phase or delegation boundary, and state where another runtime's behavior differs.
| PracHub question | Topic-specific rehearsal |
|---|---|
| Code Review of a Multi-File HTTP API: Wrong Method, Payload and Validation Bugs | Trace validation, fulfillment and the caller-visible error before changing a handler. |
| Build a Create/Execute Workflow Service With an AI Assistant: Plan, Implement, Verify | Separate an executed local dispatch test from persistence and deployment evidence. |
| Design a Logger with Independent Handlers | Compare independent handlers with a request interceptor chain; CAP dispatch differs. |
| Design an In-Memory Transactional Key-Value Store | Use transaction reasoning to explain why service after does not establish commit. |
| Design Consistent List and Category APIs | Define the response contract before adding result enrichment. |
Return to the multi-file API review and narrate a single request from validation to caller-visible result. Then remove next(), add a delayed normalizer, and move a side effect across commit. You should be able to predict each changed outcome before running a test.
Comments (0)