GraphQL Code Generator - Advanced Examples
This document provides comprehensive examples demonstrating advanced usage patterns of the GraphQL Code Generator.
E-commerce API Example
This complete example demonstrates a production-ready e-commerce API with interfaces, union types, complex type resolution, and error handling patterns.
Schema Definition
1"""Root query type for the e-commerce API."""2type Query {3"""Get a product by ID."""4product(id: ID!): Product56"""Search products with filters."""7searchProducts(8query: String9category: String10minPrice: Float11maxPrice: Float12inStock: Boolean13): ProductConnection!1415"""Get current user's shopping cart."""16cart: Cart!1718"""Get order by ID."""19order(id: ID!): Order20}2122"""Root mutation type."""23type Mutation {24"""Add item to cart."""25addToCart(input: AddToCartInput!): Cart!2627"""Place an order."""28placeOrder(input: PlaceOrderInput!): OrderResult!29}3031"""A product in the catalog."""32type Product implements Node {33id: ID!34name: String!35description: String36price: Money!37images: [ProductImage!]!38category: Category!39inventory: Inventory!40reviews: ReviewConnection!41}4243"""A product category."""44type Category implements Node {45id: ID!46name: String!47slug: String!48parent: Category49products: ProductConnection!50}5152"""Product inventory information."""53type Inventory {54inStock: Boolean!55quantity: Int!56reservedQuantity: Int!57availableQuantity: Int!58}5960"""Product image."""61type ProductImage {62url: String!63alt: String64width: Int!65height: Int!66}6768"""Monetary value with currency."""69type Money {70amount: Float!71currency: String!72formatted: String!73}7475"""Shopping cart."""76type Cart {77id: ID!78items: [CartItem!]!79subtotal: Money!80tax: Money!81total: Money!82}8384"""Item in shopping cart."""85type CartItem {86id: ID!87product: Product!88quantity: Int!89unitPrice: Money!90total: Money!91}9293"""An order."""94type Order implements Node {95id: ID!96orderNumber: String!97status: OrderStatus!98items: [OrderItem!]!99subtotal: Money!100tax: Money!101shipping: Money!102total: Money!103shippingAddress: Address!104billingAddress: Address!105createdAt: Instant!106}107108"""Order item."""109type OrderItem {110id: ID!111product: Product!112quantity: Int!113unitPrice: Money!114total: Money!115}116117"""Order status."""118enum OrderStatus {119PENDING120CONFIRMED121PROCESSING122SHIPPED123DELIVERED124CANCELLED125}126127"""Physical address."""128type Address {129street: String!130city: String!131state: String!132postalCode: String!133country: String!134}135136"""Result of placing an order."""137union OrderResult = OrderSuccess | OrderError138139"""Successful order placement."""140type OrderSuccess {141order: Order!142}143144"""Order placement error."""145type OrderError {146message: String!147code: OrderErrorCode!148}149150"""Order error codes."""151enum OrderErrorCode {152INSUFFICIENT_INVENTORY153PAYMENT_FAILED154INVALID_ADDRESS155CART_EMPTY156}157158"""Node interface for globally unique IDs."""159interface Node {160id: ID!161}162163"""Connection for paginated products."""164type ProductConnection {165edges: [ProductEdge!]!166pageInfo: PageInfo!167totalCount: Int!168}169170"""Edge in product connection."""171type ProductEdge {172node: Product!173cursor: String!174}175176"""Connection for paginated reviews."""177type ReviewConnection {178edges: [ReviewEdge!]!179pageInfo: PageInfo!180}181182"""Edge in review connection."""183type ReviewEdge {184node: Review!185cursor: String!186}187188"""Product review."""189type Review {190id: ID!191rating: Int!192title: String193comment: String194author: String!195createdAt: Instant!196}197198"""Pagination information."""199type PageInfo {200hasNextPage: Boolean!201hasPreviousPage: Boolean!202startCursor: String203endCursor: String204}205206"""Input for adding item to cart."""207input AddToCartInput {208productId: ID!209quantity: Int!210}211212"""Input for placing an order."""213input PlaceOrderInput {214shippingAddress: AddressInput!215billingAddress: AddressInput!216paymentMethodId: ID!217}218219"""Address input."""220input AddressInput {221street: String!222city: String!223state: String!224postalCode: String!225country: String!226}227228"""ISO 8601 instant scalar."""229scalar Instant230231"""Maps to existing domain model."""232directive @javaType(233"""Fully qualified Java class name."""234class: String!235) on OBJECT | INTERFACE
Code Generator Configuration
1package com.example.ecommerce.codegen;23import com.psddev.graphql.schema.codegen.GraphQLCodeGenerator;4import com.psddev.graphql.schema.types.time.InstantScalar;5import java.nio.file.Path;67public class ECommerceGraphQLCodeGenerator {89public static void main(String[] args) {10Path projectRoot = Path.of("").toAbsolutePath();1112new GraphQLCodeGenerator.Builder()13.sdlPath(projectRoot.resolve("src/main/resources/graphql/schema.graphql"))14.generatedSourcesOutputDir(projectRoot.resolve("build/generated/graphql"))15.generatedSourcesPackageName("com.example.ecommerce.generated.graphql")16.deleteExistingGeneratedSources(true)17.classNamePrefix("ECommerce")18.addCustomScalar("Instant", InstantScalar.class)19.build()20.generate();2122System.out.println("Generated GraphQL code in: " + projectRoot.resolve("build/generated/graphql"));23}24}
Implementation - Schema Context
1// code-snippets/e-commerce/ECommerceSchemaContextImpl.java23package com.example.ecommerce.graphql;45import com.example.ecommerce.generated.graphql.*;6import com.example.ecommerce.domain.*;7import com.example.ecommerce.service.*;8import javax.inject.Inject;9import java.util.List;10import java.util.stream.Collectors;1112/**13* Implementation of generated schema context.14* Connects GraphQL schema to domain services and models.15*/16public class ECommerceSchemaContextImpl extends ECommerceSchemaContext {1718private final ProductService productService;19private final CategoryService categoryService;20private final CartService cartService;21private final OrderService orderService;22private final InventoryService inventoryService;23private final ReviewService reviewService;2425@Inject26public ECommerceSchemaContextImpl(27ProductService productService,28CategoryService categoryService,29CartService cartService,30OrderService orderService,31InventoryService inventoryService,32ReviewService reviewService) {33this.productService = productService;34this.categoryService = categoryService;35this.cartService = cartService;36this.orderService = orderService;37this.inventoryService = inventoryService;38this.reviewService = reviewService;39}4041// ========================================42// Query Methods43// ========================================4445@Override46public ECommerceQuery query() {47return new ECommerceQuery() {48@Override49public ECommerceProduct product(String id) {50DomainProduct domainProduct = productService.findById(id);51return domainProduct != null ? toProduct(domainProduct) : null;52}5354@Override55public ECommerceProductConnection searchProducts(56String query,57String category,58Double minPrice,59Double maxPrice,60Boolean inStock) {6162SearchCriteria criteria = SearchCriteria.builder()63.query(query)64.category(category)65.minPrice(minPrice)66.maxPrice(maxPrice)67.inStock(inStock)68.build();6970SearchResult<DomainProduct> result = productService.search(criteria);71return toProductConnection(result);72}7374@Override75public ECommerceCart cart() {76DomainCart domainCart = cartService.getCurrentCart();77return toCart(domainCart);78}7980@Override81public ECommerceOrder order(String id) {82DomainOrder domainOrder = orderService.findById(id);83return domainOrder != null ? toOrder(domainOrder) : null;84}85};86}8788@Override89public ECommerceMutation mutation() {90return new ECommerceMutation() {91@Override92public ECommerceCart addToCart(ECommerceAddToCartInput input) {93DomainCart updatedCart = cartService.addItem(94input.productId(),95input.quantity()96);97return toCart(updatedCart);98}99100@Override101public ECommerceOrderResult placeOrder(ECommercePlaceOrderInput input) {102try {103DomainOrder order = orderService.placeOrder(104toAddressEntity(input.shippingAddress()),105toAddressEntity(input.billingAddress()),106input.paymentMethodId()107);108return toOrderSuccess(order);109} catch (InsufficientInventoryException e) {110return toOrderError(111"Insufficient inventory for one or more items",112ECommerceOrderErrorCode.INSUFFICIENT_INVENTORY113);114} catch (PaymentException e) {115return toOrderError(116"Payment processing failed: " + e.getMessage(),117ECommerceOrderErrorCode.PAYMENT_FAILED118);119} catch (InvalidAddressException e) {120return toOrderError(121"Invalid address: " + e.getMessage(),122ECommerceOrderErrorCode.INVALID_ADDRESS123);124} catch (EmptyCartException e) {125return toOrderError(126"Cannot place order with empty cart",127ECommerceOrderErrorCode.CART_EMPTY128);129}130}131};132}133134// ========================================135// Type Conversion Methods (Pattern 1)136// ========================================137138private ECommerceProduct toProduct(DomainProduct domain) {139return new ECommerceProduct() {140@Override141public String id() {142return domain.getId();143}144145@Override146public String name() {147return domain.getName();148}149150@Override151public String description() {152return domain.getDescription();153}154155@Override156public ECommerceMoney price() {157return toMoney(domain.getPrice());158}159160@Override161public List<ECommerceProductImage> images() {162return domain.getImages().stream()163.map(ECommerceSchemaContextImpl.this::toProductImage)164.collect(Collectors.toList());165}166167@Override168public ECommerceCategory category() {169return toCategory(domain.getCategory());170}171172@Override173public ECommerceInventory inventory() {174DomainInventory inv = inventoryService.getInventory(domain.getId());175return toInventory(inv);176}177178@Override179public ECommerceReviewConnection reviews() {180List<DomainReview> reviews = reviewService.getReviews(domain.getId());181return toReviewConnection(reviews);182}183};184}185186// ========================================187// Static Helper Methods (Pattern 3)188// ========================================189190private static ECommerceMoney toMoney(DomainMoney domain) {191return new ECommerceMoney() {192@Override193public Double amount() {194return domain.getAmount();195}196197@Override198public String currency() {199return domain.getCurrency();200}201202@Override203public String formatted() {204return domain.getFormatted();205}206};207}208209private static ECommerceProductImage toProductImage(DomainImage domain) {210return new ECommerceProductImage() {211@Override212public String url() {213return domain.getUrl();214}215216@Override217public String alt() {218return domain.getAlt();219}220221@Override222public Integer width() {223return domain.getWidth();224}225226@Override227public Integer height() {228return domain.getHeight();229}230};231}232233private static ECommerceAddress toAddress(DomainAddress domain) {234return new ECommerceAddress() {235@Override236public String street() {237return domain.getStreet();238}239240@Override241public String city() {242return domain.getCity();243}244245@Override246public String state() {247return domain.getState();248}249250@Override251public String postalCode() {252return domain.getPostalCode();253}254255@Override256public String country() {257return domain.getCountry();258}259};260}261262// ========================================263// Union Type Resolution (Pattern 5)264// ========================================265266private static ECommerceOrderResult toOrderSuccess(DomainOrder order) {267return new ECommerceOrderSuccess() {268@Override269public ECommerceOrder order() {270return toOrder(order);271}272};273}274275private static ECommerceOrderResult toOrderError(String message, ECommerceOrderErrorCode code) {276return new ECommerceOrderError() {277@Override278public String message() {279return message;280}281282@Override283public ECommerceOrderErrorCode code() {284return code;285}286};287}288289// ========================================290// Complex Type Conversion (Separate Classes - Pattern 2)291// ========================================292293private ECommerceCategory toCategory(DomainCategory domain) {294return new CategoryImpl(domain, categoryService);295}296297private ECommerceCart toCart(DomainCart domain) {298return new CartImpl(domain, this);299}300301private ECommerceOrder toOrder(DomainOrder domain) {302return new OrderImpl(domain);303}304305private ECommerceInventory toInventory(DomainInventory domain) {306return new InventoryImpl(domain);307}308309// ========================================310// Connection Types (Pattern 4 - Stream API)311// ========================================312313private ECommerceProductConnection toProductConnection(SearchResult<DomainProduct> result) {314return new ECommerceProductConnection() {315@Override316public List<ECommerceProductEdge> edges() {317return result.getItems().stream()318.map(product -> new ECommerceProductEdge() {319@Override320public ECommerceProduct node() {321return toProduct(product);322}323324@Override325public String cursor() {326return product.getId();327}328})329.collect(Collectors.toList());330}331332@Override333public ECommercePageInfo pageInfo() {334return new ECommercePageInfo() {335@Override336public Boolean hasNextPage() {337return result.hasNextPage();338}339340@Override341public Boolean hasPreviousPage() {342return result.hasPreviousPage();343}344345@Override346public String startCursor() {347return result.getStartCursor();348}349350@Override351public String endCursor() {352return result.getEndCursor();353}354};355}356357@Override358public Integer totalCount() {359return result.getTotalCount();360}361};362}363364private ECommerceReviewConnection toReviewConnection(List<DomainReview> reviews) {365return new ECommerceReviewConnection() {366@Override367public List<ECommerceReviewEdge> edges() {368return reviews.stream()369.map(review -> new ECommerceReviewEdge() {370@Override371public ECommerceReview node() {372return toReview(review);373}374375@Override376public String cursor() {377return review.getId();378}379})380.collect(Collectors.toList());381}382383@Override384public ECommercePageInfo pageInfo() {385// Simplified - no pagination for reviews in this example386return new ECommercePageInfo() {387@Override388public Boolean hasNextPage() {389return false;390}391392@Override393public Boolean hasPreviousPage() {394return false;395}396397@Override398public String startCursor() {399return null;400}401402@Override403public String endCursor() {404return null;405}406};407}408};409}410411private ECommerceReview toReview(DomainReview domain) {412return new ECommerceReview() {413@Override414public String id() {415return domain.getId();416}417418@Override419public Integer rating() {420return domain.getRating();421}422423@Override424public String title() {425return domain.getTitle();426}427428@Override429public String comment() {430return domain.getComment();431}432433@Override434public String author() {435return domain.getAuthor();436}437438@Override439public java.time.Instant createdAt() {440return domain.getCreatedAt();441}442};443}444445// ========================================446// Input Conversion Helpers447// ========================================448449private DomainAddress toAddressEntity(ECommerceAddressInput input) {450return DomainAddress.builder()451.street(input.street())452.city(input.city())453.state(input.state())454.postalCode(input.postalCode())455.country(input.country())456.build();457}458}
Implementation - Separate Classes (Pattern 2)
1// code-snippets/e-commerce/CategoryImpl.java23package com.example.ecommerce.graphql;45import com.example.ecommerce.generated.graphql.*;6import com.example.ecommerce.domain.*;7import com.example.ecommerce.service.CategoryService;89/**10* Separate implementation class for Category type.11* Use this pattern for complex types with many fields or that need to fetch additional data.12*/13class CategoryImpl extends ECommerceCategory {1415private final DomainCategory domain;16private final CategoryService categoryService;1718CategoryImpl(DomainCategory domain, CategoryService categoryService) {19this.domain = domain;20this.categoryService = categoryService;21}2223@Override24public String id() {25return domain.getId();26}2728@Override29public String name() {30return domain.getName();31}3233@Override34public String slug() {35return domain.getSlug();36}3738@Override39public ECommerceCategory parent() {40DomainCategory parentDomain = domain.getParent();41return parentDomain != null42? new CategoryImpl(parentDomain, categoryService)43: null;44}4546@Override47public ECommerceProductConnection products() {48// Lazy load products for this category49SearchResult<DomainProduct> result = categoryService.getProducts(domain.getId());50return ECommerceSchemaContextImpl.toProductConnection(result);51}52}
1// code-snippets/e-commerce/CartImpl.java23package com.example.ecommerce.graphql;45import com.example.ecommerce.generated.graphql.*;6import com.example.ecommerce.domain.*;7import java.util.List;8import java.util.stream.Collectors;910/**11* Separate implementation class for Cart type.12*/13class CartImpl extends ECommerceCart {1415private final DomainCart domain;16private final ECommerceSchemaContextImpl context;1718CartImpl(DomainCart domain, ECommerceSchemaContextImpl context) {19this.domain = domain;20this.context = context;21}2223@Override24public String id() {25return domain.getId();26}2728@Override29public List<ECommerceCartItem> items() {30return domain.getItems().stream()31.map(this::toCartItem)32.collect(Collectors.toList());33}3435@Override36public ECommerceMoney subtotal() {37return toMoney(domain.getSubtotal());38}3940@Override41public ECommerceMoney tax() {42return toMoney(domain.getTax());43}4445@Override46public ECommerceMoney total() {47return toMoney(domain.getTotal());48}4950private ECommerceCartItem toCartItem(DomainCartItem domainItem) {51return new ECommerceCartItem() {52@Override53public String id() {54return domainItem.getId();55}5657@Override58public ECommerceProduct product() {59return context.toProduct(domainItem.getProduct());60}6162@Override63public Integer quantity() {64return domainItem.getQuantity();65}6667@Override68public ECommerceMoney unitPrice() {69return toMoney(domainItem.getUnitPrice());70}7172@Override73public ECommerceMoney total() {74return toMoney(domainItem.getTotal());75}76};77}7879private static ECommerceMoney toMoney(DomainMoney domain) {80return new ECommerceMoney() {81@Override82public Double amount() {83return domain.getAmount();84}8586@Override87public String currency() {88return domain.getCurrency();89}9091@Override92public String formatted() {93return domain.getFormatted();94}95};96}97}
1// code-snippets/e-commerce/OrderImpl.java23package com.example.ecommerce.graphql;45import com.example.ecommerce.generated.graphql.*;6import com.example.ecommerce.domain.*;7import java.time.Instant;8import java.util.List;9import java.util.stream.Collectors;1011/**12* Separate implementation class for Order type.13*/14class OrderImpl extends ECommerceOrder {1516private final DomainOrder domain;1718OrderImpl(DomainOrder domain) {19this.domain = domain;20}2122@Override23public String id() {24return domain.getId();25}2627@Override28public String orderNumber() {29return domain.getOrderNumber();30}3132@Override33public ECommerceOrderStatus status() {34// Map domain enum to GraphQL enum35switch (domain.getStatus()) {36case PENDING: return ECommerceOrderStatus.PENDING;37case CONFIRMED: return ECommerceOrderStatus.CONFIRMED;38case PROCESSING: return ECommerceOrderStatus.PROCESSING;39case SHIPPED: return ECommerceOrderStatus.SHIPPED;40case DELIVERED: return ECommerceOrderStatus.DELIVERED;41case CANCELLED: return ECommerceOrderStatus.CANCELLED;42default:43throw new IllegalArgumentException("Unknown order status: " + domain.getStatus());44}45}4647@Override48public List<ECommerceOrderItem> items() {49return domain.getItems().stream()50.map(this::toOrderItem)51.collect(Collectors.toList());52}5354@Override55public ECommerceMoney subtotal() {56return toMoney(domain.getSubtotal());57}5859@Override60public ECommerceMoney tax() {61return toMoney(domain.getTax());62}6364@Override65public ECommerceMoney shipping() {66return toMoney(domain.getShipping());67}6869@Override70public ECommerceMoney total() {71return toMoney(domain.getTotal());72}7374@Override75public ECommerceAddress shippingAddress() {76return toAddress(domain.getShippingAddress());77}7879@Override80public ECommerceAddress billingAddress() {81return toAddress(domain.getBillingAddress());82}8384@Override85public Instant createdAt() {86return domain.getCreatedAt();87}8889private ECommerceOrderItem toOrderItem(DomainOrderItem domainItem) {90return new ECommerceOrderItem() {91@Override92public String id() {93return domainItem.getId();94}9596@Override97public ECommerceProduct product() {98// Note: Simplified - in production you might want to avoid fetching full product99return toProduct(domainItem.getProduct());100}101102@Override103public Integer quantity() {104return domainItem.getQuantity();105}106107@Override108public ECommerceMoney unitPrice() {109return toMoney(domainItem.getUnitPrice());110}111112@Override113public ECommerceMoney total() {114return toMoney(domainItem.getTotal());115}116};117}118119private static ECommerceProduct toProduct(DomainProduct domain) {120// Simplified product view for order items121return new ECommerceProduct() {122@Override123public String id() {124return domain.getId();125}126127@Override128public String name() {129return domain.getName();130}131132@Override133public String description() {134return domain.getDescription();135}136137@Override138public ECommerceMoney price() {139return toMoney(domain.getPrice());140}141142@Override143public List<ECommerceProductImage> images() {144return domain.getImages().stream()145.map(OrderImpl::toProductImage)146.collect(Collectors.toList());147}148149@Override150public ECommerceCategory category() {151// Simplified - return null or minimal category for order items152return null;153}154155@Override156public ECommerceInventory inventory() {157// Not relevant for order items158return null;159}160161@Override162public ECommerceReviewConnection reviews() {163// Not relevant for order items164return null;165}166};167}168169private static ECommerceProductImage toProductImage(DomainImage domain) {170return new ECommerceProductImage() {171@Override172public String url() {173return domain.getUrl();174}175176@Override177public String alt() {178return domain.getAlt();179}180181@Override182public Integer width() {183return domain.getWidth();184}185186@Override187public Integer height() {188return domain.getHeight();189}190};191}192193private static ECommerceMoney toMoney(DomainMoney domain) {194return new ECommerceMoney() {195@Override196public Double amount() {197return domain.getAmount();198}199200@Override201public String currency() {202return domain.getCurrency();203}204205@Override206public String formatted() {207return domain.getFormatted();208}209};210}211212private static ECommerceAddress toAddress(DomainAddress domain) {213return new ECommerceAddress() {214@Override215public String street() {216return domain.getStreet();217}218219@Override220public String city() {221return domain.getCity();222}223224@Override225public String state() {226return domain.getState();227}228229@Override230public String postalCode() {231return domain.getPostalCode();232}233234@Override235public String country() {236return domain.getCountry();237}238};239}240}
Gradle Build Integration
1plugins {2id 'java'3}45// Define source sets for generated code6sourceSets {7main {8java {9srcDir 'src/main/java'10srcDir 'build/generated/graphql'11}12}13}1415// Task to run code generator16tasks.register('generateGraphQLCode', JavaExec) {17group = 'code generation'18description = 'Generates GraphQL schema code'1920classpath = sourceSets.main.runtimeClasspath21mainClass = 'com.example.ecommerce.codegen.ECommerceGraphQLCodeGenerator'2223inputs.files(fileTree('src/main/resources/graphql'))24outputs.dir('build/generated/graphql')25}2627// Ensure code generation runs before compilation28tasks.named('compileJava') {29dependsOn('generateGraphQLCode')30}3132// Clean generated code on clean33tasks.named('clean') {34delete 'build/generated'35}3637dependencies {38// The GraphQL plugin provides both the code generator and the runtime framework39implementation 'com.brightspot.graphql:graphql'40implementation 'com.google.inject:guice:5.1.0'4142// Domain model and services43implementation project(':domain')44implementation project(':services')45}46
Key Patterns Demonstrated
This example demonstrates all recommended implementation patterns:
-
Type Mappings (CRITICAL): Money, Address, ProductImage types use static helper methods for consistent conversion across the application.
-
Separate Implementation Classes (HIGH): Complex types like Category, Cart, and Order use dedicated implementation classes for better maintainability.
-
Static Helper Methods (HIGH):
toMoney(),toAddress(),toProductImage()are reusable static methods called from multiple places. -
Stream API (HIGH): Connection types and collections use Stream API for clean, functional transformations.
-
Union Type Resolution: OrderResult union demonstrates error handling pattern with success and error types.
-
Defensive Error Handling (REQUIRED): The
placeOrdermutation shows comprehensive exception handling with specific error codes.
Multi-Schema Project Example
This example demonstrates managing multiple GraphQL schemas in a single project, useful for microservices or multi-tenant applications.
Project Structure
1project/2├── graphql-schemas/3│ ├── public-api/4│ │ └── schema.graphql5│ ├── admin-api/6│ │ └── schema.graphql7│ └── internal-api/8│ └── schema.graphql9├── codegen/10│ ├── PublicAPICodeGenerator.java11│ ├── AdminAPICodeGenerator.java12│ └── InternalAPICodeGenerator.java13└── src/main/java/14└── com/example/15├── publicapi/16│ └── PublicAPISchemaContextImpl.java17├── adminapi/18│ └── AdminAPISchemaContextImpl.java19└── internalapi/20└── InternalAPISchemaContextImpl.java
Code Generator for Each Schema
1package com.example.codegen;23import com.psddev.graphql.schema.codegen.GraphQLCodeGenerator;4import java.nio.file.Path;56public class PublicAPICodeGenerator {7public static void main(String[] args) {8Path projectRoot = Path.of("").toAbsolutePath();910new GraphQLCodeGenerator.Builder()11.sdlPath(projectRoot.resolve("graphql-schemas/public-api/schema.graphql"))12.generatedSourcesOutputDir(projectRoot.resolve("build/generated/graphql/public"))13.generatedSourcesPackageName("com.example.generated.publicapi")14.classNamePrefix("Public")15.build()16.generate();17}18}
1package com.example.codegen;23import com.psddev.graphql.schema.codegen.GraphQLCodeGenerator;4import java.nio.file.Path;56public class AdminAPICodeGenerator {7public static void main(String[] args) {8Path projectRoot = Path.of("").toAbsolutePath();910new GraphQLCodeGenerator.Builder()11.sdlPath(projectRoot.resolve("graphql-schemas/admin-api/schema.graphql"))12.generatedSourcesOutputDir(projectRoot.resolve("build/generated/graphql/admin"))13.generatedSourcesPackageName("com.example.generated.adminapi")14.classNamePrefix("Admin")15.build()16.generate();17}18}
Gradle Configuration
1sourceSets {2main {3java {4srcDir 'src/main/java'5srcDir 'build/generated/graphql/public'6srcDir 'build/generated/graphql/admin'7srcDir 'build/generated/graphql/internal'8}9}10}1112tasks.register('generatePublicAPICode', JavaExec) {13classpath = sourceSets.main.runtimeClasspath14mainClass = 'com.example.codegen.PublicAPICodeGenerator'15inputs.files(fileTree('graphql-schemas/public-api'))16outputs.dir('build/generated/graphql/public')17}1819tasks.register('generateAdminAPICode', JavaExec) {20classpath = sourceSets.main.runtimeClasspath21mainClass = 'com.example.codegen.AdminAPICodeGenerator'22inputs.files(fileTree('graphql-schemas/admin-api'))23outputs.dir('build/generated/graphql/admin')24}2526tasks.register('generateInternalAPICode', JavaExec) {27classpath = sourceSets.main.runtimeClasspath28mainClass = 'com.example.codegen.InternalAPICodeGenerator'29inputs.files(fileTree('graphql-schemas/internal-api'))30outputs.dir('build/generated/graphql/internal')31}3233tasks.register('generateAllGraphQLCode') {34dependsOn('generatePublicAPICode', 'generateAdminAPICode', 'generateInternalAPICode')35}3637tasks.named('compileJava') {38dependsOn('generateAllGraphQLCode')39}40
Batch Operations Example
Example showing how to implement batch loading and bulk operations efficiently.
Schema
1type Query {2"""Get multiple users by IDs in a single query."""3users(ids: [ID!]!): [User!]!45"""Get posts with their authors efficiently batch-loaded."""6posts(limit: Int = 10): [Post!]!7}89type Mutation {10"""Create multiple posts in a single operation."""11createPosts(inputs: [CreatePostInput!]!): CreatePostsResult!12}1314type User {15id: ID!16username: String!17email: String!18posts: [Post!]!19}2021type Post {22id: ID!23title: String!24content: String!25author: User!26}2728input CreatePostInput {29title: String!30content: String!31authorId: ID!32}3334type CreatePostsResult {35posts: [Post!]!36errors: [CreatePostError!]!37}3839type CreatePostError {40input: CreatePostInput!41message: String!42}
Implementation with DataLoader Pattern
1// code-snippets/batch/BatchSchemaContextImpl.java23package com.example.batch;45import com.example.generated.batch.*;6import com.example.domain.*;7import com.example.service.*;8import java.util.*;9import java.util.stream.Collectors;10import java.util.concurrent.CompletableFuture;1112public class BatchSchemaContextImpl extends BatchSchemaContext {1314private final UserService userService;15private final PostService postService;16private final DataLoaderRegistry dataLoaderRegistry;1718public BatchSchemaContextImpl(19UserService userService,20PostService postService,21DataLoaderRegistry dataLoaderRegistry) {22this.userService = userService;23this.postService = postService;24this.dataLoaderRegistry = dataLoaderRegistry;25}2627@Override28public BatchQuery query() {29return new BatchQuery() {30@Override31public List<BatchUser> users(List<String> ids) {32// Batch load all users in a single database query33List<DomainUser> domainUsers = userService.findByIds(ids);3435return domainUsers.stream()36.map(BatchSchemaContextImpl.this::toUser)37.collect(Collectors.toList());38}3940@Override41public List<BatchPost> posts(Integer limit) {42List<DomainPost> domainPosts = postService.findRecent(limit != null ? limit : 10);4344// Pre-load all authors in a single query to avoid N+1 problem45Set<String> authorIds = domainPosts.stream()46.map(DomainPost::getAuthorId)47.collect(Collectors.toSet());4849Map<String, DomainUser> authorsById = userService.findByIds(new ArrayList<>(authorIds))50.stream()51.collect(Collectors.toMap(DomainUser::getId, u -> u));5253return domainPosts.stream()54.map(post -> toPost(post, authorsById.get(post.getAuthorId())))55.collect(Collectors.toList());56}57};58}5960@Override61public BatchMutation mutation() {62return new BatchMutation() {63@Override64public BatchCreatePostsResult createPosts(List<BatchCreatePostInput> inputs) {65List<BatchPost> createdPosts = new ArrayList<>();66List<BatchCreatePostError> errors = new ArrayList<>();6768// Batch validation69for (BatchCreatePostInput input : inputs) {70try {71validatePostInput(input);72} catch (ValidationException e) {73errors.add(new BatchCreatePostError() {74@Override75public BatchCreatePostInput input() {76return input;77}7879@Override80public String message() {81return e.getMessage();82}83});84}85}8687// Only create valid posts in a single batch operation88if (errors.isEmpty() || createdPosts.size() > 0) {89List<BatchCreatePostInput> validInputs = inputs.stream()90.filter(input -> errors.stream().noneMatch(e -> e.input().equals(input)))91.collect(Collectors.toList());9293List<DomainPost> domainPosts = postService.createBatch(94validInputs.stream()95.map(input -> new CreatePostRequest(96input.title(),97input.content(),98input.authorId()99))100.collect(Collectors.toList())101);102103// Batch load all authors104Set<String> authorIds = domainPosts.stream()105.map(DomainPost::getAuthorId)106.collect(Collectors.toSet());107108Map<String, DomainUser> authorsById = userService.findByIds(new ArrayList<>(authorIds))109.stream()110.collect(Collectors.toMap(DomainUser::getId, u -> u));111112createdPosts = domainPosts.stream()113.map(post -> toPost(post, authorsById.get(post.getAuthorId())))114.collect(Collectors.toList());115}116117List<BatchPost> finalCreatedPosts = createdPosts;118List<BatchCreatePostError> finalErrors = errors;119120return new BatchCreatePostsResult() {121@Override122public List<BatchPost> posts() {123return finalCreatedPosts;124}125126@Override127public List<BatchCreatePostError> errors() {128return finalErrors;129}130};131}132};133}134135private BatchUser toUser(DomainUser domain) {136return new BatchUser() {137@Override138public String id() {139return domain.getId();140}141142@Override143public String username() {144return domain.getUsername();145}146147@Override148public String email() {149return domain.getEmail();150}151152@Override153public List<BatchPost> posts() {154// Lazy load posts for this user155List<DomainPost> domainPosts = postService.findByAuthor(domain.getId());156return domainPosts.stream()157.map(post -> toPost(post, domain))158.collect(Collectors.toList());159}160};161}162163private BatchPost toPost(DomainPost domainPost, DomainUser author) {164return new BatchPost() {165@Override166public String id() {167return domainPost.getId();168}169170@Override171public String title() {172return domainPost.getTitle();173}174175@Override176public String content() {177return domainPost.getContent();178}179180@Override181public BatchUser author() {182return toUser(author);183}184};185}186187private void validatePostInput(BatchCreatePostInput input) throws ValidationException {188if (input.title() == null || input.title().trim().isEmpty()) {189throw new ValidationException("Title is required");190}191if (input.content() == null || input.content().trim().isEmpty()) {192throw new ValidationException("Content is required");193}194if (input.authorId() == null) {195throw new ValidationException("Author ID is required");196}197if (!userService.exists(input.authorId())) {198throw new ValidationException("Author does not exist: " + input.authorId());199}200}201}
Key Optimization Patterns
- Batch Loading: Load multiple records in a single database query
- Pre-loading: Fetch related data upfront to avoid N+1 queries
- Batch Mutations: Create/update multiple records in one operation
- Batch Validation: Validate all inputs before processing any
- Partial Success: Return both successful operations and errors
Custom Scalar Example
Example showing how to implement and use custom scalar types.
Schema with Custom Scalars
1scalar Instant2scalar LocalDate3scalar Duration4scalar UUID5scalar URL6scalar EmailAddress7scalar PhoneNumber8scalar JSON910type Query {11event(id: UUID!): Event12}1314type Event {15id: UUID!16title: String!17startTime: Instant!18endTime: Instant!19duration: Duration!20location: URL21organizerEmail: EmailAddress!22organizerPhone: PhoneNumber23metadata: JSON24createdAt: Instant!25date: LocalDate!26}
Code Generator Configuration
1package com.example.scalars.codegen;23import com.psddev.graphql.schema.codegen.GraphQLCodeGenerator;4import com.psddev.graphql.schema.types.json.JsonScalar;5import com.psddev.graphql.schema.types.text.UrlScalar;6import com.psddev.graphql.schema.types.time.DurationScalar;7import com.psddev.graphql.schema.types.time.InstantScalar;8import com.psddev.graphql.schema.types.time.LocalDateScalar;9import com.psddev.graphql.schema.types.uuid.UuidScalar;10import java.nio.file.Path;1112public class ScalarCodeGenerator {13public static void main(String[] args) {14Path projectRoot = Path.of("").toAbsolutePath();1516new GraphQLCodeGenerator.Builder()17.sdlPath(projectRoot.resolve("src/main/resources/graphql/schema.graphql"))18.generatedSourcesOutputDir(projectRoot.resolve("build/generated/graphql"))19.generatedSourcesPackageName("com.example.generated.scalars")20.classNamePrefix("Scalar")21.addCustomScalar("Instant", InstantScalar.class)22.addCustomScalar("LocalDate", LocalDateScalar.class)23.addCustomScalar("Duration", DurationScalar.class)24.addCustomScalar("UUID", UuidScalar.class)25.addCustomScalar("URL", UrlScalar.class)26.addCustomScalar("JSON", JsonScalar.class)27.addCustomScalar(28"EmailAddress",29"com.example.scalars.EmailAddressScalar",30"com.example.generated.scalars.ScalarSchemaContext",31"java.lang.String")32.build()33.generate();34}35}
Implementation
1// code-snippets/scalars/ScalarSchemaContextImpl.java23package com.example.scalars;45import com.example.generated.scalars.*;6import com.example.domain.DomainEvent;7import com.example.service.EventService;8import com.fasterxml.jackson.databind.JsonNode;9import java.time.*;10import java.net.URL;11import java.util.UUID;1213public class ScalarSchemaContextImpl extends ScalarSchemaContext {1415private final EventService eventService;1617public ScalarSchemaContextImpl(EventService eventService) {18this.eventService = eventService;19}2021@Override22public ScalarQuery query() {23return new ScalarQuery() {24@Override25public ScalarEvent event(UUID id) {26DomainEvent domain = eventService.findById(id);27return domain != null ? toEvent(domain) : null;28}29};30}3132private ScalarEvent toEvent(DomainEvent domain) {33return new ScalarEvent() {34@Override35public UUID id() {36return domain.getId();37}3839@Override40public String title() {41return domain.getTitle();42}4344@Override45public Instant startTime() {46return domain.getStartTime();47}4849@Override50public Instant endTime() {51return domain.getEndTime();52}5354@Override55public Duration duration() {56return Duration.between(domain.getStartTime(), domain.getEndTime());57}5859@Override60public URL location() {61try {62return domain.getLocation() != null ? new URL(domain.getLocation()) : null;63} catch (Exception e) {64return null;65}66}6768@Override69public String organizerEmail() {70return domain.getOrganizerEmail();71}7273@Override74public String organizerPhone() {75return domain.getOrganizerPhone();76}7778@Override79public JsonNode metadata() {80return domain.getMetadata();81}8283@Override84public Instant createdAt() {85return domain.getCreatedAt();86}8788@Override89public LocalDate date() {90return domain.getStartTime().atZone(ZoneId.systemDefault()).toLocalDate();91}92};93}94}
Testing Generated Code
Examples of how to test your GraphQL implementation.
Unit Test Example
1package com.example.test;23import com.example.generated.blog.*;4import com.example.blog.BlogSchemaContextImpl;5import com.example.domain.*;6import com.example.service.*;7import org.junit.jupiter.api.BeforeEach;8import org.junit.jupiter.api.Test;9import org.mockito.Mock;10import org.mockito.MockitoAnnotations;1112import java.time.Instant;13import java.util.Arrays;14import java.util.List;1516import static org.junit.jupiter.api.Assertions.*;17import static org.mockito.Mockito.*;1819class BlogSchemaContextImplTest {2021@Mock22private PostService postService;2324@Mock25private UserService userService;2627private BlogSchemaContextImpl context;2829@BeforeEach30void setUp() {31MockitoAnnotations.openMocks(this);32context = new BlogSchemaContextImpl(postService, userService);33}3435@Test36void testGetPost() {37// Given38String postId = "post-123";39DomainPost domainPost = new DomainPost();40domainPost.setId(postId);41domainPost.setTitle("Test Post");42domainPost.setContent("Test content");43domainPost.setPublishedAt(Instant.now());4445DomainUser author = new DomainUser();46author.setId("user-456");47author.setUsername("testuser");48author.setEmail("test@example.com");49domainPost.setAuthor(author);5051when(postService.findById(postId)).thenReturn(domainPost);5253// When54BlogQuery query = context.query();55BlogPost result = query.post(postId);5657// Then58assertNotNull(result);59assertEquals(postId, result.id());60assertEquals("Test Post", result.title());61assertEquals("Test content", result.content());62assertNotNull(result.publishedAt());63assertNotNull(result.author());64assertEquals("testuser", result.author().username());6566verify(postService).findById(postId);67}6869@Test70void testGetPostNotFound() {71// Given72String postId = "nonexistent";73when(postService.findById(postId)).thenReturn(null);7475// When76BlogQuery query = context.query();77BlogPost result = query.post(postId);7879// Then80assertNull(result);81verify(postService).findById(postId);82}8384@Test85void testGetAllPosts() {86// Given87DomainUser author = new DomainUser();88author.setId("user-456");89author.setUsername("testuser");90author.setEmail("test@example.com");9192DomainPost post1 = new DomainPost();93post1.setId("post-1");94post1.setTitle("Post 1");95post1.setContent("Content 1");96post1.setAuthor(author);97post1.setPublishedAt(Instant.now());9899DomainPost post2 = new DomainPost();100post2.setId("post-2");101post2.setTitle("Post 2");102post2.setContent("Content 2");103post2.setAuthor(author);104post2.setPublishedAt(Instant.now());105106when(postService.findAll()).thenReturn(Arrays.asList(post1, post2));107108// When109BlogQuery query = context.query();110List<BlogPost> results = query.allPosts();111112// Then113assertNotNull(results);114assertEquals(2, results.size());115assertEquals("Post 1", results.get(0).title());116assertEquals("Post 2", results.get(1).title());117118verify(postService).findAll();119}120121@Test122void testCreatePost() {123// Given124BlogCreatePostInput input = new BlogCreatePostInput() {125@Override126public String title() {127return "New Post";128}129130@Override131public String content() {132return "New content";133}134135@Override136public String authorId() {137return "user-456";138}139};140141DomainUser author = new DomainUser();142author.setId("user-456");143author.setUsername("testuser");144145DomainPost createdPost = new DomainPost();146createdPost.setId("post-789");147createdPost.setTitle("New Post");148createdPost.setContent("New content");149createdPost.setAuthor(author);150createdPost.setPublishedAt(Instant.now());151152when(postService.create(any())).thenReturn(createdPost);153154// When155BlogMutation mutation = context.mutation();156BlogPost result = mutation.createPost(input);157158// Then159assertNotNull(result);160assertEquals("post-789", result.id());161assertEquals("New Post", result.title());162assertEquals("New content", result.content());163164verify(postService).create(any());165}166}
Integration Test Example
1package com.example.test;23import com.example.blog.BlogSchemaContextImpl;4import com.example.generated.blog.BlogSchemaLoader;5import org.junit.jupiter.api.BeforeEach;6import org.junit.jupiter.api.Test;7import org.springframework.beans.factory.annotation.Autowired;8import org.springframework.boot.test.context.SpringBootTest;9import org.springframework.boot.test.web.client.TestRestTemplate;10import org.springframework.http.HttpEntity;11import org.springframework.http.HttpHeaders;12import org.springframework.http.MediaType;1314import java.util.Map;1516import static org.junit.jupiter.api.Assertions.*;1718@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)19class GraphQLIntegrationTest {2021@Autowired22private TestRestTemplate restTemplate;2324@Test25void testQueryPost() {26// Given27String query = """28query {29post(id: "post-123") {30id31title32content33author {34username3536}37}38}39""";4041Map<String, Object> requestBody = Map.of("query", query);4243HttpHeaders headers = new HttpHeaders();44headers.setContentType(MediaType.APPLICATION_JSON);4546// When47HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);48var response = restTemplate.postForEntity("/graphql", request, Map.class);4950// Then51assertEquals(200, response.getStatusCode().value());52assertNotNull(response.getBody());5354Map<String, Object> data = (Map<String, Object>) response.getBody().get("data");55assertNotNull(data);5657Map<String, Object> post = (Map<String, Object>) data.get("post");58assertEquals("post-123", post.get("id"));59assertEquals("Test Post", post.get("title"));60}6162@Test63void testMutationCreatePost() {64// Given65String mutation = """66mutation {67createPost(input: {68title: "Integration Test Post"69content: "Content from integration test"70authorId: "user-456"71}) {72id73title74content75}76}77""";7879Map<String, Object> requestBody = Map.of("query", mutation);80HttpHeaders headers = new HttpHeaders();81headers.setContentType(MediaType.APPLICATION_JSON);8283// When84HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);85var response = restTemplate.postForEntity("/graphql", request, Map.class);8687// Then88assertEquals(200, response.getStatusCode().value());8990Map<String, Object> data = (Map<String, Object>) response.getBody().get("data");91Map<String, Object> post = (Map<String, Object>) data.get("createPost");9293assertNotNull(post.get("id"));94assertEquals("Integration Test Post", post.get("title"));95assertEquals("Content from integration test", post.get("content"));96}97}
Performance Optimization Patterns
Pattern 1: Field-Level Caching
1package com.example.performance;23import com.google.common.cache.Cache;4import com.google.common.cache.CacheBuilder;5import java.util.concurrent.TimeUnit;67public class CachingSchemaContext extends MySchemaContext {89private final Cache<String, MyProduct> productCache;10private final Cache<String, MyUser> userCache;1112public CachingSchemaContext(ProductService productService, UserService userService) {13super(productService, userService);1415// Configure caches with appropriate TTL and size limits16this.productCache = CacheBuilder.newBuilder()17.maximumSize(1000)18.expireAfterWrite(5, TimeUnit.MINUTES)19.build();2021this.userCache = CacheBuilder.newBuilder()22.maximumSize(500)23.expireAfterWrite(10, TimeUnit.MINUTES)24.build();25}2627@Override28protected MyProduct toProduct(DomainProduct domain) {29return productCache.get(domain.getId(), () -> super.toProduct(domain));30}3132@Override33protected MyUser toUser(DomainUser domain) {34return userCache.get(domain.getId(), () -> super.toUser(domain));35}3637public void invalidateProduct(String id) {38productCache.invalidate(id);39}4041public void invalidateUser(String id) {42userCache.invalidate(id);43}44}
Pattern 2: Lazy Loading with Suppliers
1package com.example.performance;23import java.util.function.Supplier;4import java.util.List;5import java.util.stream.Collectors;67public class LazyLoadingSchemaContext extends MySchemaContext {89/**10* Use Suppliers for expensive fields that might not always be accessed.11*/12private MyProduct toProductWithLazyLoading(DomainProduct domain) {13return new MyProduct() {14// Eager fields - always loaded15@Override16public String id() {17return domain.getId();18}1920@Override21public String name() {22return domain.getName();23}2425// Lazy field - only loaded if accessed26private Supplier<List<MyReview>> reviewsSupplier;2728@Override29public List<MyReview> reviews() {30if (reviewsSupplier == null) {31reviewsSupplier = () -> {32List<DomainReview> domainReviews = reviewService.getReviews(domain.getId());33return domainReviews.stream()34.map(this::toReview)35.collect(Collectors.toList());36};37}38return reviewsSupplier.get();39}4041// Lazy field with caching - loaded once if accessed42private List<MyRelatedProduct> relatedProducts;4344@Override45public List<MyRelatedProduct> relatedProducts() {46if (relatedProducts == null) {47List<DomainProduct> related = productService.findRelated(domain.getId());48relatedProducts = related.stream()49.map(LazyLoadingSchemaContext.this::toProduct)50.collect(Collectors.toList());51}52return relatedProducts;53}54};55}56}
Pattern 3: Connection Cursor-Based Pagination
1package com.example.performance;23import java.util.Base64;4import java.nio.charset.StandardCharsets;5import java.util.List;6import java.util.stream.Collectors;7import java.util.stream.IntStream;89public class PaginationSchemaContext extends MySchemaContext {1011@Override12public MyQuery query() {13return new MyQuery() {14@Override15public MyProductConnection products(Integer first, String after, Integer last, String before) {16// Decode cursors17Integer afterOffset = after != null ? decodeCursor(after) : null;18Integer beforeOffset = before != null ? decodeCursor(before) : null;1920// Calculate offset and limit21int offset = afterOffset != null ? afterOffset + 1 : 0;22int limit = first != null ? first : (last != null ? last : 20);2324// Fetch from database with offset and limit25PaginatedResult<DomainProduct> result = productService.findPaginated(offset, limit);2627return new MyProductConnection() {28@Override29public List<MyProductEdge> edges() {30List<DomainProduct> products = result.getItems();31return IntStream.range(0, products.size())32.mapToObj(index -> {33DomainProduct product = products.get(index);34return new MyProductEdge() {35@Override36public MyProduct node() {37return toProduct(product);38}3940@Override41public String cursor() {42return encodeCursor(offset + index);43}44};45})46.collect(Collectors.toList());47}4849@Override50public MyPageInfo pageInfo() {51return new MyPageInfo() {52@Override53public Boolean hasNextPage() {54return result.hasMore();55}5657@Override58public Boolean hasPreviousPage() {59return offset > 0;60}6162@Override63public String startCursor() {64return result.isEmpty() ? null : encodeCursor(offset);65}6667@Override68public String endCursor() {69return result.isEmpty() ? null70: encodeCursor(offset + result.getItems().size() - 1);71}72};73}7475@Override76public Integer totalCount() {77// Optional: Cache this or make it optional in schema78return productService.count();79}80};81}82};83}8485private String encodeCursor(int offset) {86return Base64.getEncoder().encodeToString(87String.valueOf(offset).getBytes(StandardCharsets.UTF_8)88);89}9091private Integer decodeCursor(String cursor) {92try {93byte[] decoded = Base64.getDecoder().decode(cursor);94return Integer.parseInt(new String(decoded, StandardCharsets.UTF_8));95} catch (Exception e) {96throw new IllegalArgumentException("Invalid cursor: " + cursor);97}98}99}
Summary
These advanced examples demonstrate:
- E-commerce API: Complete production-ready implementation with all recommended patterns
- Multi-Schema Projects: Managing multiple APIs in one codebase
- Batch Operations: Efficient bulk loading and mutations
- Custom Scalars: Working with extended type systems
- Testing: Unit and integration test patterns
- Performance: Caching, lazy loading, and pagination strategies
Use these patterns as reference implementations for your own GraphQL APIs.