Performance Considerations for the Whole Module
Performance Considerations for the Whole Module
~8 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026
Every performance pitfall in this module was already addressed exactly where it arises - EAV joins in block 2, observer overhead in block 4, race conditions in blocks 8/10. This chapter doesn't repeat them, it orders them by their actual cost center in the overall system: what runs on every checkout, what only matters for catalog requests, and what the nightly cron job carries on its own.
The checkout path: three observers on one event
sales_order_place_after now carries three module-owned observers: AwardPointsOnOrderPlaced (chapter 30), RedeemPointsOnOrderPlaced (chapters 63/67), and CreditPurchasedPointsPackageOnOrderPlaced (chapter 76) - all three synchronous, all three potentially issuing their own database writes. The actual cost factor is almost never the observer infrastructure itself (a single dispatch() call is microseconds), but the sum of PointsLedgerRepositoryInterface::save() and CustomerRepositoryInterface::save() calls each observer can potentially trigger - worst case (points earned and redeemed and a points package purchased) up to three ledger inserts and three customer saves in a single checkout request.
Tipp: Deliberately keeping the three observers separate (instead of merging them into one) costs almost nothing in extra time, but gains maintainability and - more importantly - fault tolerance: each observer wraps its core logic individually in try/catch (\Throwable) (chapters 30/31/63/76), so a failure in one of the three paths doesn't take the other two down with it.
EAV joins: only where they're needed
The reward collection (chapter 15) is the module's only place with real EAV joins. Chapter 18 already established the central rule: never load more attributes than the specific view actually needs (addAttributeToSelect() instead of a blanket addAttributeToSelect('*')), always filter through indexed EAV attributes, never through unbounded text search in _text tables. The GraphQL data provider (chapters 82/83) and the admin grid data source (chapter 16) both use the same collection and therefore inherit the same discipline automatically - one of the strongest practical justifications for the repository pattern from chapters 6/79: a single, correctly optimized data source instead of three independent, potentially diverging implementations.
Cache usage across the module
LoyaltyCatalogcache type (chapter 8): tag-based invalidation for the reward catalog, automatically flushed on every reward change.CacheLedgerListPlugin(chapter 38): a pure request-level cache backed by an instance array - preventsgetListByCustomerId()from running the same query twice within the same request (e.g. once for the view model, once for the widget).RedemptionRateLimiter(chapter 86): deliberately uses the genericCacheInterfaceinstead of the module's ownLoyaltyCatalogtype, since rate-limit counters need an entirely different lifetime and invalidation logic than the catalog cache.LoyaltyFeatureFlagsConfigType(chapter 88): piggybacks on the core config cache's tag instead of registering its own type - a deliberate simplification for a configuration type that changes rarely anyway.
Reconciliation queries and indexes
ReversePointsOnCreditmemoSave (chapter 31) and ExpirePoints (chapter 33) both use the same should-be-vs-is pattern with GROUP BY customer_id on the ledger. Aggregation queries like these benefit strongly from a composite index on (customer_id, type, expires_at) - noticeably more than individual indexes on each column on its own. Anyone adopting the db_schema.xml from chapter 3 in a real project should check this specifically: a plain primary key on ledger_id isn't enough for the nightly cron job as the customer base grows - the difference usually doesn't show until a few hundred thousand ledger rows, but then it shows clearly.
Achtung: Because ExpirePoints runs outside a request context (no user is waiting on an HTTP response), missing indexing goes unnoticed for a long time in everyday operation - until the cron job eventually exceeds the configured schedule_lifetime (chapter 32) and gets marked "missed". That exact symptom belongs on the troubleshooting list in chapter 103.
Cross-references to the series' pitfall chapters
- EAV performance in detail: chapter 18.
- Observer overhead and idempotency guards: chapters 29-31, 76.
- Cache types and their respective invalidation: chapters 8, 38, 86, 88.
- Race conditions on parallel redemptions (deliberately not fixed, documented): chapters 63, 81, 86.
- Reindex cost on attribute changes: chapters 19/20 (
catalog_product_attributereindex).
Performance without safeguards doesn't get you far - chapter 101 closes the loop with exactly the counterpart: a summary of every security decision made across the series.