wie Cache-Key-Faktoren kundengruppenabhängige Preise korrekt behandeln
Sobald Preise oder Sichtbarkeit von der Kundengruppe abhängen, kann ein naiver Cache dieselbe Antwort an unterschiedliche Kunden ausliefern, mit im schlechtesten Fall falschen Preisen. Dieser Artikel erklärt, wie Magentos eingebauter GraphQL-Response-Cache die Kundengruppe bereits standardmäßig in seinen Cache-Key einrechnet, wo eigene Resolver-Level-Caches trotzdem daneben liegen können, und wie sich das korrekt mit Varnish und einem vorgeschalteten CDN verträgt.
Inhaltsverzeichnis
- 1. Warum kundengruppenabhängige Preise naives Response-Caching brechen
- 2. Magentos eingebauter Mechanismus: CacheIdCalculator und Kundengruppen-Faktor
- 3. Wo es trotzdem schiefgeht: Resolver-Level-Caching als separate Schicht
- 4. Praxisbeispiel: Ein manuell gecachter Resolver ohne Kundengruppen-Anteil
- 5. Einen eigenen CacheIdFactorProviderInterface für zusätzlichen Kontext schreiben
- 6. Zusammenspiel mit Varnish: Der Header wandert in den VCL-Hash
- 7. CDN-Edge-Caching vor Varnish: Das eigentliche Risiko
- 8. Abgrenzung zum allgemeinen Varnish-GraphQL-Artikel
- 9. Testing und Verifikation gegen Preis-Leckage zwischen Kundengruppen
- 10. Zusammenfassung
- 11. FAQ
1. Warum kundengruppenabhängige Preise naives Response-Caching brechen
Sobald Preise, Sonderkonditionen oder die Sichtbarkeit einzelner Produkte von der Kundengruppe abhängen, liefert ein und dieselbe GraphQL-Query je nach anfragendem Kunden unterschiedliche, jeweils korrekte Antworten. Ein Cache, der ausschließlich nach URL oder reinem Query-Text schlüsselt, kann diesen Unterschied nicht abbilden und riskiert, die falsche Antwort für den falschen Kunden auszuliefern.
Im schlechtesten Fall bekommt eine Kundengruppe mit ausgehandelten Sonderkonditionen die für eine andere Gruppe berechnete Antwort aus dem Cache ausgeliefert, mit falschen Preisen oder fälschlich sichtbaren beziehungsweise unsichtbaren Produkten. Dieses Fehlerbild wird in Tests mit einem einzigen Testkonto leicht übersehen, weil dort niemals zwei unterschiedliche Kundengruppen gegeneinander verglichen werden.
Besonders tückisch ist, dass ein solcher Fehler in der Regel nicht sofort auffällt: Die Antwort ist syntaktisch vollkommen korrekt, die Query liefert Daten, keine Fehlermeldung deutet auf ein Problem hin. Erst ein gezielter Vergleich zwischen zwei Kundengruppen oder eine Reklamation eines Kunden, der einen falschen Preis gesehen hat, deckt das eigentliche Problem auf, weshalb kundengruppenabhängiges Caching von Anfang an mitgedacht werden sollte, statt es erst nachträglich zu reparieren.
2. Magentos eingebauter Mechanismus: CacheIdCalculator und Kundengruppen-Faktor
Seit dem Modul Magento_GraphQlCache berechnet Magento\GraphQlCache\Model\CacheId\CacheIdCalculator für jede cachefähige Anfrage einen X-Magento-Cache-Id-Wert als Hash über mehrere registrierte Faktoren. Jeder Faktor implementiert CacheIdFactorProviderInterface mit den Methoden getFactorName und getFactorValue.
Die Kundengruppe ist dabei bereits ab Werk enthalten: Magento\CustomerGraphQl\CacheIdFactorProviders\CustomerGroupProvider liefert die Kundengruppen-ID als Faktor CUSTOMER_GROUP, ergänzt um Store, Währung, Steuersatz und Login-Status aus benachbarten Providern. Der eingebaute Response-Cache trennt unterschiedliche Kundengruppen also bereits korrekt.
<?php
declare(strict_types=1);
namespace Magento\GraphQlCache\Model\CacheId;
use Magento\Framework\GraphQl\Query\Resolver\ContextInterface;
/**
* Vertrag für einen einzelnen Faktor, der in die X-Magento-Cache-Id-Berechnung einfliesst.
*/
interface CacheIdFactorProviderInterface
{
/**
* Eindeutiger Name des Faktors innerhalb der Hash-Berechnung.
*
* @return string
*/
public function getFactorName(): string;
/**
* Aktueller Wert des Faktors für den gegebenen GraphQL-Request-Kontext.
*
* @param ContextInterface $context
* @return string
*/
public function getFactorValue(ContextInterface $context): string;
}
3. Wo es trotzdem schiefgeht: Resolver-Level-Caching als separate Schicht
Der Response-Cache ist nicht die einzige Cache-Ebene: Magento_GraphQlResolverCache erlaubt das Zwischenspeichern einzelner, teurer Resolver-Ergebnisse über Resolver\IdentityInterface und einen eigenen Cache-Key-Mechanismus, unabhängig davon, ob die komplette Antwort überhaupt response-cachefähig ist.
Dieser Mechanismus kennt Kundengruppe nicht automatisch. Jeder eigene Resolver, der sich am Resolver-Cache beteiligt, muss die relevanten Kontextdimensionen selbst in seinen Cache-Key einbeziehen, sonst entsteht genau auf dieser Ebene die Vermischung, die der Response-Cache eigentlich schon verhindert.
Der Grund für diese Trennung ist nachvollziehbar: Der Response-Cache entscheidet auf Ebene der gesamten Anfrage, ob überhaupt gecached werden darf, während der Resolver-Cache gezielt einzelne, besonders teure Berechnungen innerhalb einer sonst nicht cachefähigen oder personalisierten Antwort beschleunigen soll. Genau diese Flexibilität macht ihn aber auch zur Fehlerquelle, weil er bewusst unabhängig vom Response-Cache arbeitet und dessen bereits korrekte Kundengruppen-Logik nicht automatisch erbt.
4. Praxisbeispiel: Ein manuell gecachter Resolver ohne Kundengruppen-Anteil
Ein naheliegender, in der Praxis häufiger Fehler entsteht, wenn ein Entwickler einen teuren, eigenen Resolver direkt mit CacheInterface cached, etwa das Ergebnis einer aufwendigen ERP-Preisberechnung, und dabei den Cache-Key allein aus der Produkt-SKU bildet.
Die korrigierte Version nimmt die Kundengruppen-ID aus dem GraphQL-Context als zusätzlichen Bestandteil des Cache-Keys auf, wodurch zwei Kundengruppen zwar denselben Resolver-Code, aber garantiert unterschiedliche Cache-Einträge nutzen.
<?php
declare(strict_types=1);
namespace Vendor\ErpPricing\Model\Resolver;
use Magento\Framework\App\CacheInterface;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
/**
* Beispiel-Resolver, der eine teure ERP-Preisberechnung zwischenspeichert.
*/
class ErpNegotiatedPrice implements ResolverInterface
{
/**
* @param CacheInterface $cache
*/
public function __construct(private readonly CacheInterface $cache)
{
}
/**
* @param Field $field
* @param mixed $context
* @param ResolveInfo $info
* @param array|null $value
* @param array|null $args
* @return float
*/
public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null): float
{
$sku = $value['sku'];
// FALSCH: Der Cache-Key enthält nur die SKU, alle Kundengruppen teilen sich denselben Eintrag.
// $cacheKey = 'erp_price_' . $sku;
// KORREKT: Die Kundengruppen-ID aus dem GraphQL-Context ergänzt den Cache-Key,
// damit zwei Kundengruppen niemals denselben Preis-Cache-Eintrag nutzen.
$customerGroupId = $context->getExtensionAttributes()->getCustomerGroupId();
$cacheKey = 'erp_price_' . $sku . '_group_' . $customerGroupId;
$cached = $this->cache->load($cacheKey);
if ($cached !== false) {
return (float) $cached;
}
$price = $this->fetchNegotiatedPriceFromErp($sku, $customerGroupId);
$this->cache->save((string) $price, $cacheKey, [], 600);
return $price;
}
/**
* @param string $sku
* @param int $customerGroupId
* @return float
*/
private function fetchNegotiatedPriceFromErp(string $sku, int $customerGroupId): float
{
// ... teurer ERP-Aufruf, hier stark vereinfacht dargestellt
return 0.0;
}
}
5. Einen eigenen CacheIdFactorProviderInterface für zusätzlichen Kontext schreiben
Reicht die Kundengruppe allein nicht aus, etwa weil B2B-Firmen innerhalb derselben Kundengruppe unterschiedliche, ausgehandelte Preise über einen Shared Catalog erhalten, lässt sich ein zusätzlicher Faktor analog zum Kern-Muster ergänzen.
Die Registrierung erfolgt als weiteres Array-Item am Konstruktor-Argument idFactorProviders von CacheIdCalculator, exakt nach demselben Muster, mit dem der Kern bereits Store-, Währungs- und Kundengruppen-Provider verdrahtet.
<?php
declare(strict_types=1);
namespace Vendor\B2bCaching\Model\CacheIdFactorProviders;
use Magento\Framework\GraphQl\Query\Resolver\ContextInterface;
use Magento\GraphQlCache\Model\CacheId\CacheIdFactorProviderInterface;
/**
* Ergänzt die B2B-Firmen-ID als eigenen Faktor in der Cache-Id-Berechnung,
* für Fälle, in denen dieselbe Kundengruppe unterschiedliche Firmenpreise hat.
*/
class CompanyIdProvider implements CacheIdFactorProviderInterface
{
/**
* @return string
*/
public function getFactorName(): string
{
return 'COMPANY_ID';
}
/**
* @param ContextInterface $context
* @return string
*/
public function getFactorValue(ContextInterface $context): string
{
return (string) ($context->getExtensionAttributes()->getCompanyId() ?? '0');
}
}
6. Zusammenspiel mit Varnish: Der Header wandert in den VCL-Hash
Das mitgelieferte Varnish-VCL übernimmt den von Magento berechneten X-Magento-Cache-Id-Header direkt in die eigene Hash-Berechnung, ergänzt um Store- und Content-Currency-Header sowie einen Zusatz für authentifizierte Anfragen mit Bearer-Token.
Varnish muss dafür selbst nichts über Kundengruppen wissen, es verlässt sich vollständig darauf, dass Magento die Berechnung korrekt vornimmt. Ein Response wird zusätzlich nur dann überhaupt gecached, wenn der im Request gesendete Cache-Id-Wert mit dem in der Antwort übereinstimmt.
# Auszug aus dem mitgelieferten varnish6.vcl, vereinfacht
sub process_graphql_headers {
if (req.http.X-Magento-Cache-Id) {
hash_data(req.http.X-Magento-Cache-Id);
hash_data(req.http.Store);
hash_data(req.http.Content-Currency);
if (req.http.Authorization) {
hash_data("Authorized");
}
}
}
7. CDN-Edge-Caching vor Varnish: Das eigentliche Risiko
Ein zusätzliches CDN vor Varnish bringt eigene Caching-Regeln mit, die sich nicht zwangsläufig an Magentos Header halten. Cached ein CDN GraphQL-Antworten allein nach URL, etwa bei GET-Anfragen mit Persisted Queries, kann es trotz korrektem Magento- und Varnish-Verhalten gruppenübergreifend falsche Antworten ausliefern.
Die Lösung besteht entweder darin, das CDN so zu konfigurieren, dass die relevanten Magento-Header explizit in dessen eigenen Cache-Schlüssel einfließen, oder GraphQL-Anfragen am CDN grundsätzlich vom Caching auszunehmen und diese Aufgabe vollständig Varnish zu überlassen.
8. Abgrenzung zum allgemeinen Varnish-GraphQL-Artikel
Ein allgemeiner Artikel zur Varnish-Konfiguration für GraphQL behandelt typischerweise Grundlagen wie VCL-Aufbau, Cache-Tags für Invalidierung und Timeouts. Dieser Artikel setzt genau dort an, wo die Kundengruppe konkret in die Cache-Key-Berechnung einfließt, sowohl auf Magento- als auch auf CDN-Ebene.
Wer beide Themen kombiniert, sollte zuerst die grundsätzliche Varnish-Anbindung sicherstellen und erst danach die hier beschriebenen, kundengruppenspezifischen Faktoren und die Resolver-Cache-Fallstricke prüfen, weil eine fehlerhafte Grundkonfiguration jede weitere Optimierung wertlos macht.
9. Testing und Verifikation gegen Preis-Leckage zwischen Kundengruppen
Der zuverlässigste Test führt dieselbe Query mit zwei unterschiedlichen, authentifizierten Kundengruppen aus und vergleicht sowohl die gelieferten Preise als auch den zurückgegebenen X-Magento-Cache-Id-Wert, beide Werte müssen sich zwischen den Gruppen unterscheiden.
Ergänzend lohnt sich ein automatisierter Regressionstest genau für diesen Vergleich, damit ein künftiger, unbedacht hinzugefügter manueller Resolver-Cache nicht unbemerkt wieder genau die Lücke öffnet, die der eingebaute Mechanismus eigentlich schon geschlossen hat.
Sinnvoll ist außerdem ein regelmäßiger, automatisierter Check direkt in Produktion, der stichprobenartig denselben Vergleich mit zwei echten Testkonten unterschiedlicher Kundengruppen durchführt und bei einer Abweichung sofort Alarm schlägt, statt sich allein auf einmalige Tests zum Zeitpunkt eines Deployments zu verlassen. Gerade Änderungen an Drittmodulen, die eigene Resolver oder eigene Caching-Logik mitbringen, können diese Garantie später wieder unbemerkt aushebeln, weshalb ein solcher Produktionscheck fester Bestandteil des laufenden Monitorings bleiben sollte statt einer einmaligen Abnahmeprüfung.
Bei einem Audit lohnt sich zusätzlich ein bewusster Blick auf jeden Drittanbieter-Resolver im Schema, mit der einfachen Leitfrage, ob und wie er cached, und ob die Kundengruppe dabei tatsächlich Teil des verwendeten Cache-Keys ist. Diese kurze, aber gezielte Prüfung deckt erfahrungsgemäß deutlich mehr potenzielle Preis-Leckagen auf als ein rein oberflächlicher Blick auf die Gesamtarchitektur des Caching.
| Schicht | Mechanismus | Kundengruppe automatisch enthalten? | Zuständig für |
|---|---|---|---|
| GraphQL-Response-Cache | CacheIdCalculator plus CacheIdFactorProviderInterface | Ja, über CustomerGroupProvider | Kompletten Response cachen und wiederverwenden |
| Resolver- oder Feld-Cache | Magento_GraphQlResolverCache, Resolver\IdentityInterface | Nein, muss pro Resolver ergänzt werden | Einzelne teure Feldergebnisse cachen |
| Eigener manueller Resolver-Cache | Direkter Aufruf von CacheInterface im Resolver-Code | Nein, nur wenn Entwickler es einbaut | Ad-hoc-Caching in Custom-Resolvern |
| Varnish | VCL-Hash über X-Magento-Cache-Id, Store, Content-Currency | Ja, indirekt über den übernommenen Header | Edge-Caching kompletter HTTP-Responses |
| Externes CDN vor Varnish | Abhängig von der CDN-Konfiguration | Nur wenn Header explizit respektiert werden | Globales Edge-Caching |
Mironsoft
Magento-Entwicklung, Modul-Beratung und Systemarchitektur
Magento-Projekt, das eine zweite Meinung oder erfahrene Umsetzung braucht?
Wir entwickeln individuelle Magento-Module, beraten bei Architekturentscheidungen und übernehmen komplexe Umsetzungen, von der Service-Contract-Planung bis zum produktionsreifen Deployment.
Architektur-Beratung
Modul- und Systemarchitektur vor der Umsetzung fundiert durchdenken lassen.
Custom-Modul-Entwicklung
Individuelle Magento-Module nach Best Practices sauber umsetzen.
Code-Review & Audit
Bestehende Module auf Performance, Sicherheit und Wartbarkeit prüfen lassen.
10. Zusammenfassung
GraphQL-Caching pro Kundengruppe in Magento: Das Wichtigste auf einen Blick
Guter Startpunkt
Der eingebaute Response-Cache berücksichtigt Kundengruppe bereits standardmäßig über CustomerGroupProvider.
Echtes Risiko
Eigene, manuell gecachte Resolver-Ergebnisse ohne Kundengruppen-Anteil im Cache-Key.
Erweiterung
Zusätzliche Kontext-Dimensionen wie B2B-Firmen-ID über einen eigenen CacheIdFactorProviderInterface ergänzen.
Edge-Ebene
Varnish übernimmt den von Magento berechneten Header, ein CDN davor muss ihn explizit respektieren.