{ }
type
GraphQL · Schema Design · Interfaces · Unions · Magento
GraphQL Schema-Design:
Felder, Typen, Interfaces und Unions sinnvoll aufbauen

Ein schlecht designtes Schema ist schwer zu erweitern, erzwingt Breaking Changes und macht Resolver komplex. Die richtigen Nullability-Entscheidungen, Custom Scalars und der korrekte Einsatz von Interfaces und Unions entscheiden über die Langlebigkeit einer GraphQL-API.

20 Min. Lesezeit Interfaces · Unions · Custom Scalars · Nullability · Schema-First SDL · Magento GraphQL · graphqls-Dateien

1. Schema-First als Entwicklungsstrategie

Bei Schema-First wird das GraphQL-Schema in SDL (Schema Definition Language) als Vertragsartefakt definiert, bevor Frontend- oder Backend-Implementierung beginnt. Das Schema ist das gemeinsame Dokument zwischen Frontend-Entwicklern, die wissen müssen, welche Daten verfügbar sind, und Backend-Entwicklern, die wissen müssen, was implementiert werden muss. Im Gegensatz zu Code-First – bei dem das Schema aus dem Code generiert wird – zwingt Schema-First alle Beteiligten, Datenmodelle und Schnittstellen explizit zu diskutieren, bevor Code geschrieben wird.

Das praktische Ergebnis: Missverständnisse zwischen Teams werden früh aufgedeckt. Ein Frontend-Entwickler, der ein Feld discountPercent: Float erwartet, während der Backend-Entwickler discount_percent: Int implementiert, bemerkt diesen Konflikt sofort, wenn beide das Schema als Basis nehmen. In Magento manifestiert sich Schema-First in den .graphqls-Dateien, die jedes Modul definiert. Das Magento-Schema selbst ist ein gutes Beispiel für Schema-First-Design: alle GraphQL-Typen sind in SDL-Dateien definiert und durch den Magento-Schema-Stitching-Mechanismus zusammengeführt.

2. Nullability: die häufig unterschätzte Design-Entscheidung

Nullability in GraphQL ist eine bewusste Design-Entscheidung mit weitreichenden Konsequenzen. In GraphQL ist jedes Feld standardmäßig nullable – das bedeutet, ein Resolver darf null zurückgeben, ohne einen Fehler auszulösen. Non-nullable Felder werden mit ! gekennzeichnet (String!). Der Unterschied ist fundamental: wenn ein Non-nullable Feld null zurückgibt, propagiert GraphQL den Fehler nach oben und nullt das nächstgültige nullable Feld. In tiefen Typstrukturen kann das dazu führen, dass ein einzelner fehlgeschlagener Resolver den gesamten Teilbaum der Response auf null setzt.

Die Faustregel für Nullability-Entscheidungen: Felder, die immer vorhanden sein müssen, damit die API sinnvoll nutzbar ist, werden als Non-nullable definiert (id: ID!, sku: String!). Felder, die konzeptuell optional sind oder aus einem sekundären Datenquellenaufruf stammen, bleiben nullable. Zu viele Non-nullable Felder machen das Schema starr und erhöhen die Fehleranfälligkeit bei partiellen Datenbankausfällen. Zu viele nullable Felder zwingen das Frontend zu ständigen Null-Checks. Ein ausgewogenes Design nimmt Pflichtfelder als Non-nullable und optionale Kontextdaten als nullable.


# Nullability-Design: bewusste Entscheidungen für jeden Feldtyp

type Product {
  # Non-nullable: ohne diese Felder ist das Produkt nicht darstellbar
  id: ID!
  sku: String!
  name: String!
  price_range: PriceRange!
  url_key: String!

  # Nullable: optionale Metadaten, kein Fehler wenn leer
  description: String
  meta_title: String
  short_description: String

  # Nullable mit Reason: stammt aus separatem Service-Aufruf
  # Kann null sein bei Service-Ausfall ohne den Produkt-Resolver zu brechen
  review_summary: ReviewSummary

  # Non-nullable Liste, aber nullable Listenelemente
  # Liste ist immer vorhanden (mindestens leer), Elemente könnten null sein
  media_gallery: [MediaGalleryInterface]!

  # Non-nullable Liste mit Non-nullable Elementen
  # Garantiert: Liste vorhanden und alle Elemente vorhanden
  categories: [CategoryInterface!]!
}

# Falsch: alles non-nullable macht den Resolver fragil
type ProductFragile {
  id: ID!
  sku: String!
  review_summary: ReviewSummary!  # Bricht, wenn Review-Service down
  description: String!            # Bricht, wenn kein Description-Wert
}

3. Custom Scalars statt generischer String-Typen

Custom Scalars lösen ein häufiges Schema-Problem: semantisch bedeutungsvolle Werte, die als primitive Typen modelliert werden. Ein Datum als String, eine URL als String, ein JSON-Blob als String – all das funktioniert technisch, gibt dem Client aber keine Typinformation darüber, wie er mit dem Wert umgehen soll. Custom Scalars fügen semantische Bedeutung hinzu: DateTime statt String signalisiert, dass ein ISO-8601-Datum erwartet wird. URL statt String kommuniziert, dass der Wert eine valide URL ist. Das verbessert nicht nur die Dokumentation, sondern ermöglicht Client-seitige Validierung durch Code-Generation-Tools.

Magento nutzt Custom Scalars an mehreren Stellen: Money für Geldbeträge (mit Wert und Währung), obwohl das dort als Object-Typ statt als Scalar gelöst ist. Für eigene Module empfiehlt sich der Custom Scalar DateTime für alle Zeitstempel statt String, PhoneNumber für Telefonnummern mit impliziter Formatvalidierung und JSON für strukturierte Daten, die in einem Feld gespeichert werden sollen, ohne dafür einen vollständigen Typen zu definieren. Letzteres sollte sparsam eingesetzt werden – ein expliziter Typ ist fast immer besser als ein JSON-Blob.

4. Interfaces: gemeinsame Felder typisieren

GraphQL Interfaces definieren eine Menge von Feldern, die alle implementierenden Typen aufweisen müssen. Sie lösen das Problem, wenn mehrere Typen konzeptuell verwandt sind und gemeinsame Felder teilen – diese Gemeinsamkeit wird im Schema explizit ausgedrückt, statt in jedem Typ einzeln zu definieren. Magento nutzt Interfaces extensiv: ProductInterface definiert alle Felder, die jedes Produkt haben muss, während SimpleProduct, ConfigurableProduct und BundleProduct dieses Interface implementieren und eigene zusätzliche Felder ergänzen.

Das Schlüsselprinzip beim Interface-Design: das Interface enthält nur Felder, die für alle implementierenden Typen sinnvoll sind. Ein Feld, das nur für konfigurierbare Produkte existiert (configurable_options), gehört nicht ins ProductInterface, sondern in den konkreten Typ ConfigurableProduct. Überladene Interfaces, die Felder aller möglichen Implementierungen enthalten, werden schwergewichtig und erschweren neue Implementierungen – weil jede neue Implementierung alle Felder bedienen muss, auch wenn semantisch sinnlos.


# Interface-Design: gemeinsame Felder klar definieren

interface ProductInterface {
  # Fields required by ALL product types
  id: ID!
  sku: String!
  name: String!
  url_key: String!
  price_range: PriceRange!
  stock_status: ProductStockStatus!
  categories: [CategoryInterface]
  media_gallery: [MediaGalleryInterface]
}

# Each concrete type implements the interface and adds its own fields
type SimpleProduct implements ProductInterface {
  id: ID!
  sku: String!
  name: String!
  url_key: String!
  price_range: PriceRange!
  stock_status: ProductStockStatus!
  categories: [CategoryInterface]
  media_gallery: [MediaGalleryInterface]
  # SimpleProduct-specific:
  weight: Float
  only_x_left_in_stock: Float
}

type ConfigurableProduct implements ProductInterface {
  id: ID!
  sku: String!
  name: String!
  url_key: String!
  price_range: PriceRange!
  stock_status: ProductStockStatus!
  categories: [CategoryInterface]
  media_gallery: [MediaGalleryInterface]
  # ConfigurableProduct-specific:
  configurable_options: [ConfigurableProductOption]
  variants: [ConfigurableVariant]
}

# Client query using inline fragments on the interface
query GetProducts($search: String!) {
  products(search: $search) {
    items {
      __typename
      sku
      name
      price_range { minimum_price { final_price { value currency } } }
      ... on ConfigurableProduct {
        configurable_options { attribute_code label }
      }
      ... on SimpleProduct {
        only_x_left_in_stock
      }
    }
  }
}

5. Unions: heterogene Ergebnismengen

GraphQL Unions modellieren Ergebnismengen, die Werte unterschiedlicher Typen ohne gemeinsame Felder enthalten können. Der typische Anwendungsfall ist eine Suche, die verschiedene Entitätstypen zurückgeben kann: Produkte, Kategorien, CMS-Seiten. Diese Typen teilen keine sinnvollen gemeinsamen Felder – eine CMS-Seite hat keinen SKU, ein Produkt keinen Seiteninhalt. Trotzdem will der Client sie in einer einzigen Antwort empfangen. Unions ermöglichen genau das: union SearchResult = Product | Category | CmsPage.

Unions können keine gemeinsamen Felder deklarieren – der Client muss immer über __typename und Inline-Fragmente auf die konkreten Typen zugreifen. Das ist ein entscheidender Unterschied zu Interfaces. Wenn zwei Typen tatsächlich gemeinsame Felder haben, ist ein Interface die bessere Wahl – Unions sind für wirklich heterogene Mengen ohne semantische Überlappung. Magento verwendet Unions beispielsweise für Checkout-Zahlungsmethoden-Konfigurationen, wo PayPal-Konfiguration und Kreditkarten-Konfiguration völlig unterschiedliche Felder haben.

6. Interface vs. Union: wann was?

Die Entscheidung zwischen Interface und Union ist eine der häufigsten Designfragen beim Aufbau eines GraphQL-Schemas. Die Leitfrage: Gibt es gemeinsame Felder, die alle Typen aufweisen? Ja → Interface. Nein → Union. Wenn unterschiedliche Entitäten gemeinsame Grundfelder haben, die der Client ohne Inline-Fragmentierung nutzen kann, ermöglicht ein Interface flüssigere Queries. Wenn die Typen fundamental verschieden sind und kein sinnvolles gemeinsames Feld existiert, ist eine Union klarer.

Ein Grenzfall: manchmal haben Typen ein einziges gemeinsames Feld (etwa id: ID!), sind aber ansonsten völlig verschieden. In diesem Fall sollte man prüfen, ob das gemeinsame Feld tatsächlich semantisch identisch ist: die ID eines Produkts und die ID einer CMS-Seite sind technisch beide IDs, aber konzeptuell aus verschiedenen Namenräumen. Hier ist eine Union oft die ehrlichere Modellierung – ein Interface nur für ein technisches Feld wäre künstlich. Wenn in Zukunft mehr gemeinsame Felder hinzukommen könnten, ist ein Interface zukunftssicherer.

7. Schema-Design in Magento: graphqls-Dateien und Erweiterbarkeit

Magento implementiert GraphQL-Schema-Design über .graphqls-Dateien, die in jedem Modul im Verzeichnis etc/schema.graphqls liegen. Das Besondere: Magento fügt diese Schemata zur Laufzeit zusammen und erlaubt es, bestehende Typen zu erweitern (type Query wird in jedem Modul ergänzt). Das ist eine mächtige Erweiterbarkeit, hat aber auch Fallstricke: wenn ein Modul einen bestehenden Typ mit einem Feld ergänzt, das denselben Namen hat wie ein Feld aus einem anderen Modul, entsteht ein Konflikt, der schwer zu debuggen ist.

Das beste Schema-Design-Pattern für Magento-Module: eigene Typ-Namespaces verwenden. Statt ein generisches Feld extra_info zum ProductInterface hinzuzufügen, ein modulspezifisches Feld mironsoft_product_meta: MironsoftProductMeta ergänzen. Der Typ MironsoftProductMeta gehört vollständig dem Modul und kann ohne Konflikte mit anderen Modulen weiterentwickelt werden. Außerdem empfiehlt sich die Nutzung von @resolver-Direktiven in der graphqls-Datei, die direkt auf die zuständige PHP-Resolver-Klasse zeigen – so ist der Zusammenhang zwischen Schema und Implementierung sofort sichtbar.


# Magento-Modul-Schema: etc/schema.graphqls
# Erweiterung des ProductInterface mit modulspezifischem Namespace

# Bestehenden Typ erweitern ohne Konflikte:
type Query {
  # Module-specific query — no naming conflict risk
  mironsoftProductRecommendations(
    sku: String!
    limit: Int = 5
  ): MironsoftRecommendationResult! @resolver(class: "Mironsoft\\Catalog\\Model\\Resolver\\ProductRecommendations")
}

# Module-owned type: can be evolved independently
type MironsoftRecommendationResult {
  items: [MironsoftRecommendedProduct!]!
  algorithm: String!
  generated_at: String!
}

type MironsoftRecommendedProduct {
  sku: String!
  score: Float!
  reason: MironsoftRecommendationReason!
  product: ProductInterface! @resolver(class: "Mironsoft\\Catalog\\Model\\Resolver\\RecommendedProductLoader")
}

enum MironsoftRecommendationReason {
  FREQUENTLY_BOUGHT_TOGETHER
  SIMILAR_ATTRIBUTES
  VIEWED_TOGETHER
  PRICE_RANGE_MATCH
}

# Extending ProductInterface — using module-namespaced field
type ProductInterface {
  mironsoft_sustainability_score: Float @resolver(class: "Mironsoft\\Catalog\\Model\\Resolver\\SustainabilityScore")
  mironsoft_lead_time_days: Int @resolver(class: "Mironsoft\\Catalog\\Model\\Resolver\\LeadTime")
}

8. Schema-Design-Entscheidungen im Vergleich

Die wichtigsten Schema-Design-Entscheidungen haben direkte Auswirkungen auf die Wartbarkeit, Erweiterbarkeit und Performance eines GraphQL-Schemas. Dieser Vergleich zeigt häufige Fehlentscheidungen und ihre bessere Alternative.

Design-Entscheidung Problematisches Muster Empfohlenes Muster Begründung
Typsicherheit date: String date: DateTime Custom Scalar signalisiert Format, ermöglicht Code-Gen-Validierung
Nullability Alles Non-nullable (!) Pflichtfelder non-nullable, optionale nullable Zu viele ! macht Schema starr und bricht bei partiellen Fehlern
Polymorphie Alles in einem Typ mit vielen nullable Feldern Interface oder Union je nach Gemeinsamkeiten Klare Typstruktur verbessert Tooling und Dokumentation
Magento-Erweiterung Generische Feldnamen ohne Namespace Modulspezifischer Präfix (mironsoft_*) Verhindert Namenskonflikte mit anderen Modulen
Entwicklungsstrategie Code-First: Schema aus Code generiert Schema-First: SDL als Vertragsartefakt Frühe Abstimmung zwischen Teams, Mock-Server sofort möglich

Das häufigste Anti-Pattern bei Magento-GraphQL-Schema-Design ist das undifferenzierte Erweitern des ProductInterface mit Feldern ohne Namespace. Nach mehreren Jahren und mehreren Modulen entsteht so ein überladenes Interface mit Feldern verschiedener Herkunft ohne klare Zuordnung. Das Namespacing-Pattern – modulspezifische Felder mit Modul-Präfix – löst dieses Problem dauerhaft, ohne Schema-Flexibilität zu opfern.

9. Zusammenfassung

Gutes GraphQL-Schema-Design trifft bewusste Entscheidungen bei Nullability, Typen, Interfaces und Unions. Nullability bestimmt, wie tolerant das Schema gegenüber partiellen Fehlern ist – zu viele Non-nullable Felder machen das Schema fragil. Custom Scalars geben primitiven Werten semantische Bedeutung und verbessern Tooling und Dokumentation. Interfaces modellieren gemeinsame Felder verwandter Typen und ermöglichen typsichere Queries ohne vollständige Inline-Fragmentierung. Unions modellieren heterogene Ergebnismengen ohne erzwungene gemeinsame Felder.

In Magento wird Schema-Design durch .graphqls-Dateien umgesetzt, die beim Deployment zusammengeführt werden. Modulspezifische Namespaces verhindern Konflikte zwischen Modulen und machen den Schema-Ursprung jedes Feldes nachvollziehbar. Schema-First als Entwicklungsstrategie stellt sicher, dass das Schema als Vertragsartefakt zwischen Frontend- und Backend-Teams steht, bevor Implementierung beginnt.

GraphQL Schema-Design — Das Wichtigste auf einen Blick

Nullability bewusst wählen

Pflichtfelder non-nullable (id!, sku!), optionale Kontextdaten nullable. Zu viele ! macht das Schema fragil bei partiellen Datenbankausfällen.

Custom Scalars statt String

DateTime, URL, PhoneNumber statt generischer Strings – verbessert Dokumentation, Code-Generation und Client-seitige Validierung.

Interface vs. Union

Gemeinsame Felder → Interface. Fundamental unterschiedliche Typen ohne gemeinsame Felder → Union. Beides löst Polymorphie-Probleme, aber für unterschiedliche Fälle.

Magento-Namespace-Pattern

Modulfelder mit Präfix (mironsoft_*) – verhindert Namenskonflikte zwischen Modulen und macht Feld-Herkunft im Schema sofort erkennbar.

11. FAQ: GraphQL Schema-Design – Felder, Typen, Interfaces und Unions

1Interface vs. Union: der wichtigste Unterschied?
Interfaces haben gemeinsame Felder, die alle implementierenden Typen aufweisen. Unions haben keine gemeinsamen Felder. Faustregel: gemeinsame Felder = Interface, fundamental unterschiedliche Typen = Union.
2Wann nullable, wann non-nullable?
Pflichtfelder ohne die das Objekt nicht darstellbar ist (id!, sku!) = non-nullable. Optionale Kontextdaten, sekundäre Datenquellen = nullable. Zu viele ! macht das Schema fragil.
3Wann Custom Scalars statt String?
Wenn ein Wert semantische Bedeutung über den primitiven Typ hinaus hat: DateTime, URL, PhoneNumber, Email. Verbessert Dokumentation, Code-Generation und client-seitige Validierung.
4Magento-Modul ohne Namenskonflikte erweitern?
Modulspezifischen Präfix verwenden: mironsoft_field_name. So sind Felder eindeutig einem Modul zugeordnet und Konflikte zwischen Modulen unmöglich.
5Was ist Schema-First-Entwicklung?
SDL als Vertragsartefakt vor Implementierungsbeginn definieren. Ermöglicht sofort Mock-Server, frühe Team-Abstimmung und klare Verantwortung für Felder.
6Wie verwaltet Magento mehrere Schema-Dateien?
Jedes Modul definiert etc/schema.graphqls. Magento fügt diese zur Laufzeit zusammen. Namenskonflikte entstehen bei gleichen Feldnamen aus verschiedenen Modulen.
7Interface für nur ein gemeinsames Feld?
Fast nie sinnvoll. Ein Interface nur für ein technisches Feld wie id ist meist künstlich. Union ist die ehrlichere Modellierung – außer zukünftige gemeinsame Felder sind absehbar.
8Nullability und Fehlerbehandlung?
Non-nullable Feld mit null-Rückgabe propagiert Fehler nach oben zum nächsten nullable Elternfeld. Tiefe non-nullable Hierarchien können große Response-Teile auf null setzen.
9Union nachträglich in Interface umwandeln?
Breaking Change. @deprecated auf der alten Union, neues Interface parallel anbieten. Erst nach Ablauf des Deprecation-Zeitraums entfernen, wenn alle Clients migriert sind.
10Welche Felder ins Interface, welche in den Typ?
Interface: nur Felder, die für ALLE Implementierungen semantisch sinnvoll sind. Konkreter Typ: typenspezifische Felder. Ein Feld, das nur für einen von fünf Typen gilt, gehört nicht ins Interface.