Performance Engineering for SAP Commerce: Targets, Load Models, and the Tuning Loop
How to make performance a process instead of a go-live surprise: NFR definition, the PI/s arithmetic, realistic user journeys and volumetrics, CCv2-specific load test pitfalls, and the scientific tuning cycle that actually converges.
Performance problems on commerce projects follow a script: nobody talks about performance until UAT, then production-sized data arrives, then the first realistic load hits, and suddenly the go-live date is negotiating with a 4-second product page. The fix is not heroics in the last month; it is treating performance as an engineering discipline from sprint one: explicit targets, a load model based on real numbers, tests running continuously, and a tuning loop that changes one thing at a time. This guide merges two CX Works pieces into that discipline, end to end.
Requirements: The NFRs Nobody Writes Down#
Functional requirements get workshops; non-functional requirements get assumed, and assumed NFRs are how "the search works" ships alongside "the search takes six seconds". Write the NFRs with the same rigour as features, and make each one:
- Explicit and measurable: "95 percent of requests under 200 ms at 600 concurrent users" is an NFR; "the site should be fast" is a wish
- Commercially oriented: tied to conversion, capacity for the marketing plan, peak-event survival
- Practical and within budget: an NFR nobody can afford to test is decoration
Three interdependent numbers define the storefront's performance envelope: response time per request, page impressions per second (PI/s), and maximum concurrent users before CPU saturation. Fix any two and the third follows. The arithmetic everyone should carry around:
One core rendering pages at 200 ms produces 5 PI/s (1000 ms / 200 ms). That 200 ms is the whole request: CPU, database, Solr, network, waits. A real trace of a cached category page illustrates the decomposition: 121 ms total response, made of roughly 12 ms code execution, 53 ms Solr, 56 ms database, 53 ms network I/O, with several of those overlapping. The lesson inside the numbers: on most commerce requests your Java code is a rounding error; the request lives and dies on Solr, the database, and whatever external calls you added. Optimize accordingly.
Concurrency links to PI/s through session behaviour:
Total PI/s = (concurrent users x requests per session) / session length in seconds
A standard target worth defaulting to: 95 percent of requests under 200 ms, acknowledging the remaining 5 percent are the legitimately heavy operations (checkout, large carts) plus housekeeping noise (GC pauses).
The Load Model: Journeys and Volumetrics#
A load test is only as honest as its model of reality. Two inputs:
User journeys#
Real customers do not teleport to a PDP and buy. They search, browse, hesitate, abandon. Model the journeys with their think times and distributions; the classic B2C trio (for which the accelerator ships sample JMeter scripts, generated under your storefront test module's resources/jmeter) is a sane starting split:
| Journey | Share | Behaviour |
|---|---|---|
| Browser | ~55% | Homepage, store finder, category pages, PDPs, searches; buys nothing |
| Abandoned cart | ~40% | Browses, adds to cart, edits cart, leaves |
| Checkout | ~5% | Adds an item, adjusts quantity, completes checkout |
Feed the distributions from your analytics (GA4 gives you exact funnel drop-offs; see the analytics guide). Greenfield projects proxy from the legacy system, ERP order data, or marketing forecasts, and then correct the model with real data after launch. The share numbers matter more than they look: cart and checkout requests are the expensive ones, so a model that overweights browsers flatters your results.
Volumetrics#
Size the data and the growth, or the test measures a system that will not exist in production. The tables to fill in, for now plus two years:
| Item | Example |
|---|---|
| Items in database | 17M |
| Registered users | 95K |
| Enabled promotions | 45 |
| Searches per session | 3 |
| Page visits per session | 9 |
| Metric | Year 0 | +1 | +2 |
|---|---|---|---|
| Orders/year (base) | 200K | 250K | 300K |
| Orders peak hour (base) | 68 | 86 | 102 |
| Peak PI/s (base) | 32 | 40 | 43 |
| Orders/day (event peak) | 3K | 4K | 5K |
| Peak PI/s (event peak) | 127 | 131 | 135 |
The two-year horizon is the point: capacity decisions (aspect sizing, Solr topology, cache regions) should be made against year +2 peak, not launch-day base.
Test Readiness: What Must Be True Before the First Run#
- A committed test environment, production-shaped: same aspect layout, production-sized data (generate synthetic records to reach volume), production configuration. Testing a 50k-product environment to predict a 17M-product production is astrology.
- APM instrumented and understood. Dynatrace comes with every CCv2 subscription; someone on the team must actually know it before the first load test, because the test's entire value is in the traces (the companion Dynatrace guide covers the workflow).
- Load generation ready: tooling (JMeter, Gatling, k6), scripted journeys, and somewhere to keep and compare results across runs.
- A named owner. Performance analysis is a role, not a side effect. The RACI does not need to be ceremonious, but "who reads the results and decides the next change" must have exactly one answer, and leadership approval paths for infrastructure changes should be agreed before testing starts, because a tuning loop that waits a week per change request is not a loop.
Running Load Tests on CCv2: The Pitfalls List#
Platform-specific traps that ruin otherwise good tests, each learned expensively by someone:
- Warm up before measuring. First minutes fill caches and JIT-compile hot paths; include a warm-up phase and discard it from results.
- The liveness probe can kill your test. CCv2 probes nodes every couple of seconds; a burst of long-running requests (unpaginated wishlists, slow recommendation calls) can exhaust Tomcat's HTTP threads, fail the probe repeatedly, and restart the pod, mid-test. If your load test triggers restarts, you have found a production bug, not a test problem: real traffic will do the same.
- Short transactions or a dead connection pool. Database connections are held for the transaction's duration; long transactions under load deplete the pool and everything queues behind it. This is the load-test symptom of the transaction rules in the code review playbook.
- Un-block the test data. Scripted journeys fail in waves when stock runs out or test users get disabled; keep Groovy scripts handy to reset stock levels and user states between runs.
- Rich, varied data. A test that hammers the same 20 products measures your cache, not your platform. The product spread in the journeys must resemble real traffic's long tail.
- Mock the weak back-ends deliberately. When a downstream system (test ERP, sandbox PSP) cannot take production-scale load, mock it, and record that the mock exists: the integration's real capacity becomes an explicitly tracked risk instead of a silent gap in the test.
The Tuning Loop#
With a baseline capacity test done (ramp users until CPU saturation; note where the 95th percentile crosses your target), the loop begins:
- Pick the worst offender against target. Every request class over 200 ms, ordered by traffic-weighted impact.
- Decompose it in the APM. CPU versus database versus Solr versus external calls, and look especially for high variance between invocations of the same request: variance means contention or cache misses, not steady cost.
- Hypothesize, change one thing, retest. Lazy-load a fragment, add a cache region, fix an N+1, paginate the unbounded call, scale a tier. One change per run, delta recorded, ineffective changes reverted. The discipline sounds slow and is the fastest path there is; three simultaneous changes that "helped" teach you nothing and cost you the ability to revert.
- Beware the linearity assumption. Doubling storefront nodes does not double throughput when the database or an external service is the constraint; the ratio of throughput to response time bends as you approach shared bottlenecks. Scaling hardware is a valid fix only after the traces say the bottleneck is actually the tier being scaled.
- Stop at the target, not at perfection. Optimization has brutally diminishing returns; the NFR is the finish line, and effort past it is stolen from features.
Run the whole cycle iteratively until targets hold at year +2 peak volumetrics.
Make It Continuous or Watch It Regress#
A one-off performance project decays immediately; the sprint after the tuning sprint adds the next slow query. Two habits keep the gains:
- Load tests in the pipeline. A weekly (or per-release) scripted run against the performance environment, with trends tracked: 95th percentile per journey, PI/s at reference load, error rates. The question the trend answers is not "are we fast" but "did this week make us slower", which is answerable and actionable while the offending change is one diff old.
- Watch the back-office too. Cronjobs, imports, syncs, and indexing share the database with your storefront; run them during storefront load tests on the schedule they will really have. A nightly full sync that is invisible at 3 a.m. is very visible when a marketing email moves peak traffic into its window (the high-traffic guide covers the operational side of this).
Reporting: Turning Runs into Decisions#
Each test cycle ends with a short, opinionated document, not a data dump:
- NFR scoreboard: met, not met, by how much, with the trend
- The two or three smoking guns, with their traces, and the recommended single next change for each
- Expectations honestly set: what further tuning can realistically deliver versus what needs backlog-level changes (data model, architecture, integration redesign)
- Charts of the deltas, because "the checkout p95 went from 1.8 s to 420 ms" lands harder with the picture
The performance engineer's real deliverable is prioritization: pointing the team's limited attention at the two issues that matter commercially, sprint after sprint. Done that way, performance stops being the crisis before go-live and becomes what it should be: a number on a dashboard that has been boring for months.