Skip to main content
Version: 1.2.x

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."""
2
type Query {
3
"""Get a product by ID."""
4
product(id: ID!): Product
5
6
"""Search products with filters."""
7
searchProducts(
8
query: String
9
category: String
10
minPrice: Float
11
maxPrice: Float
12
inStock: Boolean
13
): ProductConnection!
14
15
"""Get current user's shopping cart."""
16
cart: Cart!
17
18
"""Get order by ID."""
19
order(id: ID!): Order
20
}
21
22
"""Root mutation type."""
23
type Mutation {
24
"""Add item to cart."""
25
addToCart(input: AddToCartInput!): Cart!
26
27
"""Place an order."""
28
placeOrder(input: PlaceOrderInput!): OrderResult!
29
}
30
31
"""A product in the catalog."""
32
type Product implements Node {
33
id: ID!
34
name: String!
35
description: String
36
price: Money!
37
images: [ProductImage!]!
38
category: Category!
39
inventory: Inventory!
40
reviews: ReviewConnection!
41
}
42
43
"""A product category."""
44
type Category implements Node {
45
id: ID!
46
name: String!
47
slug: String!
48
parent: Category
49
products: ProductConnection!
50
}
51
52
"""Product inventory information."""
53
type Inventory {
54
inStock: Boolean!
55
quantity: Int!
56
reservedQuantity: Int!
57
availableQuantity: Int!
58
}
59
60
"""Product image."""
61
type ProductImage {
62
url: String!
63
alt: String
64
width: Int!
65
height: Int!
66
}
67
68
"""Monetary value with currency."""
69
type Money {
70
amount: Float!
71
currency: String!
72
formatted: String!
73
}
74
75
"""Shopping cart."""
76
type Cart {
77
id: ID!
78
items: [CartItem!]!
79
subtotal: Money!
80
tax: Money!
81
total: Money!
82
}
83
84
"""Item in shopping cart."""
85
type CartItem {
86
id: ID!
87
product: Product!
88
quantity: Int!
89
unitPrice: Money!
90
total: Money!
91
}
92
93
"""An order."""
94
type Order implements Node {
95
id: ID!
96
orderNumber: String!
97
status: OrderStatus!
98
items: [OrderItem!]!
99
subtotal: Money!
100
tax: Money!
101
shipping: Money!
102
total: Money!
103
shippingAddress: Address!
104
billingAddress: Address!
105
createdAt: Instant!
106
}
107
108
"""Order item."""
109
type OrderItem {
110
id: ID!
111
product: Product!
112
quantity: Int!
113
unitPrice: Money!
114
total: Money!
115
}
116
117
"""Order status."""
118
enum OrderStatus {
119
PENDING
120
CONFIRMED
121
PROCESSING
122
SHIPPED
123
DELIVERED
124
CANCELLED
125
}
126
127
"""Physical address."""
128
type Address {
129
street: String!
130
city: String!
131
state: String!
132
postalCode: String!
133
country: String!
134
}
135
136
"""Result of placing an order."""
137
union OrderResult = OrderSuccess | OrderError
138
139
"""Successful order placement."""
140
type OrderSuccess {
141
order: Order!
142
}
143
144
"""Order placement error."""
145
type OrderError {
146
message: String!
147
code: OrderErrorCode!
148
}
149
150
"""Order error codes."""
151
enum OrderErrorCode {
152
INSUFFICIENT_INVENTORY
153
PAYMENT_FAILED
154
INVALID_ADDRESS
155
CART_EMPTY
156
}
157
158
"""Node interface for globally unique IDs."""
159
interface Node {
160
id: ID!
161
}
162
163
"""Connection for paginated products."""
164
type ProductConnection {
165
edges: [ProductEdge!]!
166
pageInfo: PageInfo!
167
totalCount: Int!
168
}
169
170
"""Edge in product connection."""
171
type ProductEdge {
172
node: Product!
173
cursor: String!
174
}
175
176
"""Connection for paginated reviews."""
177
type ReviewConnection {
178
edges: [ReviewEdge!]!
179
pageInfo: PageInfo!
180
}
181
182
"""Edge in review connection."""
183
type ReviewEdge {
184
node: Review!
185
cursor: String!
186
}
187
188
"""Product review."""
189
type Review {
190
id: ID!
191
rating: Int!
192
title: String
193
comment: String
194
author: String!
195
createdAt: Instant!
196
}
197
198
"""Pagination information."""
199
type PageInfo {
200
hasNextPage: Boolean!
201
hasPreviousPage: Boolean!
202
startCursor: String
203
endCursor: String
204
}
205
206
"""Input for adding item to cart."""
207
input AddToCartInput {
208
productId: ID!
209
quantity: Int!
210
}
211
212
"""Input for placing an order."""
213
input PlaceOrderInput {
214
shippingAddress: AddressInput!
215
billingAddress: AddressInput!
216
paymentMethodId: ID!
217
}
218
219
"""Address input."""
220
input AddressInput {
221
street: String!
222
city: String!
223
state: String!
224
postalCode: String!
225
country: String!
226
}
227
228
"""ISO 8601 instant scalar."""
229
scalar Instant
230
231
"""Maps to existing domain model."""
232
directive @javaType(
233
"""Fully qualified Java class name."""
234
class: String!
235
) on OBJECT | INTERFACE

Code Generator Configuration

1
package com.example.ecommerce.codegen;
2
3
import com.psddev.graphql.schema.codegen.GraphQLCodeGenerator;
4
import com.psddev.graphql.schema.types.time.InstantScalar;
5
import java.nio.file.Path;
6
7
public class ECommerceGraphQLCodeGenerator {
8
9
public static void main(String[] args) {
10
Path projectRoot = Path.of("").toAbsolutePath();
11
12
new 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();
21
22
System.out.println("Generated GraphQL code in: " + projectRoot.resolve("build/generated/graphql"));
23
}
24
}

Implementation - Schema Context

1
// code-snippets/e-commerce/ECommerceSchemaContextImpl.java
2
3
package com.example.ecommerce.graphql;
4
5
import com.example.ecommerce.generated.graphql.*;
6
import com.example.ecommerce.domain.*;
7
import com.example.ecommerce.service.*;
8
import javax.inject.Inject;
9
import java.util.List;
10
import java.util.stream.Collectors;
11
12
/**
13
* Implementation of generated schema context.
14
* Connects GraphQL schema to domain services and models.
15
*/
16
public class ECommerceSchemaContextImpl extends ECommerceSchemaContext {
17
18
private final ProductService productService;
19
private final CategoryService categoryService;
20
private final CartService cartService;
21
private final OrderService orderService;
22
private final InventoryService inventoryService;
23
private final ReviewService reviewService;
24
25
@Inject
26
public ECommerceSchemaContextImpl(
27
ProductService productService,
28
CategoryService categoryService,
29
CartService cartService,
30
OrderService orderService,
31
InventoryService inventoryService,
32
ReviewService reviewService) {
33
this.productService = productService;
34
this.categoryService = categoryService;
35
this.cartService = cartService;
36
this.orderService = orderService;
37
this.inventoryService = inventoryService;
38
this.reviewService = reviewService;
39
}
40
41
// ========================================
42
// Query Methods
43
// ========================================
44
45
@Override
46
public ECommerceQuery query() {
47
return new ECommerceQuery() {
48
@Override
49
public ECommerceProduct product(String id) {
50
DomainProduct domainProduct = productService.findById(id);
51
return domainProduct != null ? toProduct(domainProduct) : null;
52
}
53
54
@Override
55
public ECommerceProductConnection searchProducts(
56
String query,
57
String category,
58
Double minPrice,
59
Double maxPrice,
60
Boolean inStock) {
61
62
SearchCriteria criteria = SearchCriteria.builder()
63
.query(query)
64
.category(category)
65
.minPrice(minPrice)
66
.maxPrice(maxPrice)
67
.inStock(inStock)
68
.build();
69
70
SearchResult<DomainProduct> result = productService.search(criteria);
71
return toProductConnection(result);
72
}
73
74
@Override
75
public ECommerceCart cart() {
76
DomainCart domainCart = cartService.getCurrentCart();
77
return toCart(domainCart);
78
}
79
80
@Override
81
public ECommerceOrder order(String id) {
82
DomainOrder domainOrder = orderService.findById(id);
83
return domainOrder != null ? toOrder(domainOrder) : null;
84
}
85
};
86
}
87
88
@Override
89
public ECommerceMutation mutation() {
90
return new ECommerceMutation() {
91
@Override
92
public ECommerceCart addToCart(ECommerceAddToCartInput input) {
93
DomainCart updatedCart = cartService.addItem(
94
input.productId(),
95
input.quantity()
96
);
97
return toCart(updatedCart);
98
}
99
100
@Override
101
public ECommerceOrderResult placeOrder(ECommercePlaceOrderInput input) {
102
try {
103
DomainOrder order = orderService.placeOrder(
104
toAddressEntity(input.shippingAddress()),
105
toAddressEntity(input.billingAddress()),
106
input.paymentMethodId()
107
);
108
return toOrderSuccess(order);
109
} catch (InsufficientInventoryException e) {
110
return toOrderError(
111
"Insufficient inventory for one or more items",
112
ECommerceOrderErrorCode.INSUFFICIENT_INVENTORY
113
);
114
} catch (PaymentException e) {
115
return toOrderError(
116
"Payment processing failed: " + e.getMessage(),
117
ECommerceOrderErrorCode.PAYMENT_FAILED
118
);
119
} catch (InvalidAddressException e) {
120
return toOrderError(
121
"Invalid address: " + e.getMessage(),
122
ECommerceOrderErrorCode.INVALID_ADDRESS
123
);
124
} catch (EmptyCartException e) {
125
return toOrderError(
126
"Cannot place order with empty cart",
127
ECommerceOrderErrorCode.CART_EMPTY
128
);
129
}
130
}
131
};
132
}
133
134
// ========================================
135
// Type Conversion Methods (Pattern 1)
136
// ========================================
137
138
private ECommerceProduct toProduct(DomainProduct domain) {
139
return new ECommerceProduct() {
140
@Override
141
public String id() {
142
return domain.getId();
143
}
144
145
@Override
146
public String name() {
147
return domain.getName();
148
}
149
150
@Override
151
public String description() {
152
return domain.getDescription();
153
}
154
155
@Override
156
public ECommerceMoney price() {
157
return toMoney(domain.getPrice());
158
}
159
160
@Override
161
public List<ECommerceProductImage> images() {
162
return domain.getImages().stream()
163
.map(ECommerceSchemaContextImpl.this::toProductImage)
164
.collect(Collectors.toList());
165
}
166
167
@Override
168
public ECommerceCategory category() {
169
return toCategory(domain.getCategory());
170
}
171
172
@Override
173
public ECommerceInventory inventory() {
174
DomainInventory inv = inventoryService.getInventory(domain.getId());
175
return toInventory(inv);
176
}
177
178
@Override
179
public ECommerceReviewConnection reviews() {
180
List<DomainReview> reviews = reviewService.getReviews(domain.getId());
181
return toReviewConnection(reviews);
182
}
183
};
184
}
185
186
// ========================================
187
// Static Helper Methods (Pattern 3)
188
// ========================================
189
190
private static ECommerceMoney toMoney(DomainMoney domain) {
191
return new ECommerceMoney() {
192
@Override
193
public Double amount() {
194
return domain.getAmount();
195
}
196
197
@Override
198
public String currency() {
199
return domain.getCurrency();
200
}
201
202
@Override
203
public String formatted() {
204
return domain.getFormatted();
205
}
206
};
207
}
208
209
private static ECommerceProductImage toProductImage(DomainImage domain) {
210
return new ECommerceProductImage() {
211
@Override
212
public String url() {
213
return domain.getUrl();
214
}
215
216
@Override
217
public String alt() {
218
return domain.getAlt();
219
}
220
221
@Override
222
public Integer width() {
223
return domain.getWidth();
224
}
225
226
@Override
227
public Integer height() {
228
return domain.getHeight();
229
}
230
};
231
}
232
233
private static ECommerceAddress toAddress(DomainAddress domain) {
234
return new ECommerceAddress() {
235
@Override
236
public String street() {
237
return domain.getStreet();
238
}
239
240
@Override
241
public String city() {
242
return domain.getCity();
243
}
244
245
@Override
246
public String state() {
247
return domain.getState();
248
}
249
250
@Override
251
public String postalCode() {
252
return domain.getPostalCode();
253
}
254
255
@Override
256
public String country() {
257
return domain.getCountry();
258
}
259
};
260
}
261
262
// ========================================
263
// Union Type Resolution (Pattern 5)
264
// ========================================
265
266
private static ECommerceOrderResult toOrderSuccess(DomainOrder order) {
267
return new ECommerceOrderSuccess() {
268
@Override
269
public ECommerceOrder order() {
270
return toOrder(order);
271
}
272
};
273
}
274
275
private static ECommerceOrderResult toOrderError(String message, ECommerceOrderErrorCode code) {
276
return new ECommerceOrderError() {
277
@Override
278
public String message() {
279
return message;
280
}
281
282
@Override
283
public ECommerceOrderErrorCode code() {
284
return code;
285
}
286
};
287
}
288
289
// ========================================
290
// Complex Type Conversion (Separate Classes - Pattern 2)
291
// ========================================
292
293
private ECommerceCategory toCategory(DomainCategory domain) {
294
return new CategoryImpl(domain, categoryService);
295
}
296
297
private ECommerceCart toCart(DomainCart domain) {
298
return new CartImpl(domain, this);
299
}
300
301
private ECommerceOrder toOrder(DomainOrder domain) {
302
return new OrderImpl(domain);
303
}
304
305
private ECommerceInventory toInventory(DomainInventory domain) {
306
return new InventoryImpl(domain);
307
}
308
309
// ========================================
310
// Connection Types (Pattern 4 - Stream API)
311
// ========================================
312
313
private ECommerceProductConnection toProductConnection(SearchResult<DomainProduct> result) {
314
return new ECommerceProductConnection() {
315
@Override
316
public List<ECommerceProductEdge> edges() {
317
return result.getItems().stream()
318
.map(product -> new ECommerceProductEdge() {
319
@Override
320
public ECommerceProduct node() {
321
return toProduct(product);
322
}
323
324
@Override
325
public String cursor() {
326
return product.getId();
327
}
328
})
329
.collect(Collectors.toList());
330
}
331
332
@Override
333
public ECommercePageInfo pageInfo() {
334
return new ECommercePageInfo() {
335
@Override
336
public Boolean hasNextPage() {
337
return result.hasNextPage();
338
}
339
340
@Override
341
public Boolean hasPreviousPage() {
342
return result.hasPreviousPage();
343
}
344
345
@Override
346
public String startCursor() {
347
return result.getStartCursor();
348
}
349
350
@Override
351
public String endCursor() {
352
return result.getEndCursor();
353
}
354
};
355
}
356
357
@Override
358
public Integer totalCount() {
359
return result.getTotalCount();
360
}
361
};
362
}
363
364
private ECommerceReviewConnection toReviewConnection(List<DomainReview> reviews) {
365
return new ECommerceReviewConnection() {
366
@Override
367
public List<ECommerceReviewEdge> edges() {
368
return reviews.stream()
369
.map(review -> new ECommerceReviewEdge() {
370
@Override
371
public ECommerceReview node() {
372
return toReview(review);
373
}
374
375
@Override
376
public String cursor() {
377
return review.getId();
378
}
379
})
380
.collect(Collectors.toList());
381
}
382
383
@Override
384
public ECommercePageInfo pageInfo() {
385
// Simplified - no pagination for reviews in this example
386
return new ECommercePageInfo() {
387
@Override
388
public Boolean hasNextPage() {
389
return false;
390
}
391
392
@Override
393
public Boolean hasPreviousPage() {
394
return false;
395
}
396
397
@Override
398
public String startCursor() {
399
return null;
400
}
401
402
@Override
403
public String endCursor() {
404
return null;
405
}
406
};
407
}
408
};
409
}
410
411
private ECommerceReview toReview(DomainReview domain) {
412
return new ECommerceReview() {
413
@Override
414
public String id() {
415
return domain.getId();
416
}
417
418
@Override
419
public Integer rating() {
420
return domain.getRating();
421
}
422
423
@Override
424
public String title() {
425
return domain.getTitle();
426
}
427
428
@Override
429
public String comment() {
430
return domain.getComment();
431
}
432
433
@Override
434
public String author() {
435
return domain.getAuthor();
436
}
437
438
@Override
439
public java.time.Instant createdAt() {
440
return domain.getCreatedAt();
441
}
442
};
443
}
444
445
// ========================================
446
// Input Conversion Helpers
447
// ========================================
448
449
private DomainAddress toAddressEntity(ECommerceAddressInput input) {
450
return 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.java
2
3
package com.example.ecommerce.graphql;
4
5
import com.example.ecommerce.generated.graphql.*;
6
import com.example.ecommerce.domain.*;
7
import com.example.ecommerce.service.CategoryService;
8
9
/**
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
*/
13
class CategoryImpl extends ECommerceCategory {
14
15
private final DomainCategory domain;
16
private final CategoryService categoryService;
17
18
CategoryImpl(DomainCategory domain, CategoryService categoryService) {
19
this.domain = domain;
20
this.categoryService = categoryService;
21
}
22
23
@Override
24
public String id() {
25
return domain.getId();
26
}
27
28
@Override
29
public String name() {
30
return domain.getName();
31
}
32
33
@Override
34
public String slug() {
35
return domain.getSlug();
36
}
37
38
@Override
39
public ECommerceCategory parent() {
40
DomainCategory parentDomain = domain.getParent();
41
return parentDomain != null
42
? new CategoryImpl(parentDomain, categoryService)
43
: null;
44
}
45
46
@Override
47
public ECommerceProductConnection products() {
48
// Lazy load products for this category
49
SearchResult<DomainProduct> result = categoryService.getProducts(domain.getId());
50
return ECommerceSchemaContextImpl.toProductConnection(result);
51
}
52
}
1
// code-snippets/e-commerce/CartImpl.java
2
3
package com.example.ecommerce.graphql;
4
5
import com.example.ecommerce.generated.graphql.*;
6
import com.example.ecommerce.domain.*;
7
import java.util.List;
8
import java.util.stream.Collectors;
9
10
/**
11
* Separate implementation class for Cart type.
12
*/
13
class CartImpl extends ECommerceCart {
14
15
private final DomainCart domain;
16
private final ECommerceSchemaContextImpl context;
17
18
CartImpl(DomainCart domain, ECommerceSchemaContextImpl context) {
19
this.domain = domain;
20
this.context = context;
21
}
22
23
@Override
24
public String id() {
25
return domain.getId();
26
}
27
28
@Override
29
public List<ECommerceCartItem> items() {
30
return domain.getItems().stream()
31
.map(this::toCartItem)
32
.collect(Collectors.toList());
33
}
34
35
@Override
36
public ECommerceMoney subtotal() {
37
return toMoney(domain.getSubtotal());
38
}
39
40
@Override
41
public ECommerceMoney tax() {
42
return toMoney(domain.getTax());
43
}
44
45
@Override
46
public ECommerceMoney total() {
47
return toMoney(domain.getTotal());
48
}
49
50
private ECommerceCartItem toCartItem(DomainCartItem domainItem) {
51
return new ECommerceCartItem() {
52
@Override
53
public String id() {
54
return domainItem.getId();
55
}
56
57
@Override
58
public ECommerceProduct product() {
59
return context.toProduct(domainItem.getProduct());
60
}
61
62
@Override
63
public Integer quantity() {
64
return domainItem.getQuantity();
65
}
66
67
@Override
68
public ECommerceMoney unitPrice() {
69
return toMoney(domainItem.getUnitPrice());
70
}
71
72
@Override
73
public ECommerceMoney total() {
74
return toMoney(domainItem.getTotal());
75
}
76
};
77
}
78
79
private static ECommerceMoney toMoney(DomainMoney domain) {
80
return new ECommerceMoney() {
81
@Override
82
public Double amount() {
83
return domain.getAmount();
84
}
85
86
@Override
87
public String currency() {
88
return domain.getCurrency();
89
}
90
91
@Override
92
public String formatted() {
93
return domain.getFormatted();
94
}
95
};
96
}
97
}
1
// code-snippets/e-commerce/OrderImpl.java
2
3
package com.example.ecommerce.graphql;
4
5
import com.example.ecommerce.generated.graphql.*;
6
import com.example.ecommerce.domain.*;
7
import java.time.Instant;
8
import java.util.List;
9
import java.util.stream.Collectors;
10
11
/**
12
* Separate implementation class for Order type.
13
*/
14
class OrderImpl extends ECommerceOrder {
15
16
private final DomainOrder domain;
17
18
OrderImpl(DomainOrder domain) {
19
this.domain = domain;
20
}
21
22
@Override
23
public String id() {
24
return domain.getId();
25
}
26
27
@Override
28
public String orderNumber() {
29
return domain.getOrderNumber();
30
}
31
32
@Override
33
public ECommerceOrderStatus status() {
34
// Map domain enum to GraphQL enum
35
switch (domain.getStatus()) {
36
case PENDING: return ECommerceOrderStatus.PENDING;
37
case CONFIRMED: return ECommerceOrderStatus.CONFIRMED;
38
case PROCESSING: return ECommerceOrderStatus.PROCESSING;
39
case SHIPPED: return ECommerceOrderStatus.SHIPPED;
40
case DELIVERED: return ECommerceOrderStatus.DELIVERED;
41
case CANCELLED: return ECommerceOrderStatus.CANCELLED;
42
default:
43
throw new IllegalArgumentException("Unknown order status: " + domain.getStatus());
44
}
45
}
46
47
@Override
48
public List<ECommerceOrderItem> items() {
49
return domain.getItems().stream()
50
.map(this::toOrderItem)
51
.collect(Collectors.toList());
52
}
53
54
@Override
55
public ECommerceMoney subtotal() {
56
return toMoney(domain.getSubtotal());
57
}
58
59
@Override
60
public ECommerceMoney tax() {
61
return toMoney(domain.getTax());
62
}
63
64
@Override
65
public ECommerceMoney shipping() {
66
return toMoney(domain.getShipping());
67
}
68
69
@Override
70
public ECommerceMoney total() {
71
return toMoney(domain.getTotal());
72
}
73
74
@Override
75
public ECommerceAddress shippingAddress() {
76
return toAddress(domain.getShippingAddress());
77
}
78
79
@Override
80
public ECommerceAddress billingAddress() {
81
return toAddress(domain.getBillingAddress());
82
}
83
84
@Override
85
public Instant createdAt() {
86
return domain.getCreatedAt();
87
}
88
89
private ECommerceOrderItem toOrderItem(DomainOrderItem domainItem) {
90
return new ECommerceOrderItem() {
91
@Override
92
public String id() {
93
return domainItem.getId();
94
}
95
96
@Override
97
public ECommerceProduct product() {
98
// Note: Simplified - in production you might want to avoid fetching full product
99
return toProduct(domainItem.getProduct());
100
}
101
102
@Override
103
public Integer quantity() {
104
return domainItem.getQuantity();
105
}
106
107
@Override
108
public ECommerceMoney unitPrice() {
109
return toMoney(domainItem.getUnitPrice());
110
}
111
112
@Override
113
public ECommerceMoney total() {
114
return toMoney(domainItem.getTotal());
115
}
116
};
117
}
118
119
private static ECommerceProduct toProduct(DomainProduct domain) {
120
// Simplified product view for order items
121
return new ECommerceProduct() {
122
@Override
123
public String id() {
124
return domain.getId();
125
}
126
127
@Override
128
public String name() {
129
return domain.getName();
130
}
131
132
@Override
133
public String description() {
134
return domain.getDescription();
135
}
136
137
@Override
138
public ECommerceMoney price() {
139
return toMoney(domain.getPrice());
140
}
141
142
@Override
143
public List<ECommerceProductImage> images() {
144
return domain.getImages().stream()
145
.map(OrderImpl::toProductImage)
146
.collect(Collectors.toList());
147
}
148
149
@Override
150
public ECommerceCategory category() {
151
// Simplified - return null or minimal category for order items
152
return null;
153
}
154
155
@Override
156
public ECommerceInventory inventory() {
157
// Not relevant for order items
158
return null;
159
}
160
161
@Override
162
public ECommerceReviewConnection reviews() {
163
// Not relevant for order items
164
return null;
165
}
166
};
167
}
168
169
private static ECommerceProductImage toProductImage(DomainImage domain) {
170
return new ECommerceProductImage() {
171
@Override
172
public String url() {
173
return domain.getUrl();
174
}
175
176
@Override
177
public String alt() {
178
return domain.getAlt();
179
}
180
181
@Override
182
public Integer width() {
183
return domain.getWidth();
184
}
185
186
@Override
187
public Integer height() {
188
return domain.getHeight();
189
}
190
};
191
}
192
193
private static ECommerceMoney toMoney(DomainMoney domain) {
194
return new ECommerceMoney() {
195
@Override
196
public Double amount() {
197
return domain.getAmount();
198
}
199
200
@Override
201
public String currency() {
202
return domain.getCurrency();
203
}
204
205
@Override
206
public String formatted() {
207
return domain.getFormatted();
208
}
209
};
210
}
211
212
private static ECommerceAddress toAddress(DomainAddress domain) {
213
return new ECommerceAddress() {
214
@Override
215
public String street() {
216
return domain.getStreet();
217
}
218
219
@Override
220
public String city() {
221
return domain.getCity();
222
}
223
224
@Override
225
public String state() {
226
return domain.getState();
227
}
228
229
@Override
230
public String postalCode() {
231
return domain.getPostalCode();
232
}
233
234
@Override
235
public String country() {
236
return domain.getCountry();
237
}
238
};
239
}
240
}

Gradle Build Integration

1
plugins {
2
id 'java'
3
}
4
5
// Define source sets for generated code
6
sourceSets {
7
main {
8
java {
9
srcDir 'src/main/java'
10
srcDir 'build/generated/graphql'
11
}
12
}
13
}
14
15
// Task to run code generator
16
tasks.register('generateGraphQLCode', JavaExec) {
17
group = 'code generation'
18
description = 'Generates GraphQL schema code'
19
20
classpath = sourceSets.main.runtimeClasspath
21
mainClass = 'com.example.ecommerce.codegen.ECommerceGraphQLCodeGenerator'
22
23
inputs.files(fileTree('src/main/resources/graphql'))
24
outputs.dir('build/generated/graphql')
25
}
26
27
// Ensure code generation runs before compilation
28
tasks.named('compileJava') {
29
dependsOn('generateGraphQLCode')
30
}
31
32
// Clean generated code on clean
33
tasks.named('clean') {
34
delete 'build/generated'
35
}
36
37
dependencies {
38
// The GraphQL plugin provides both the code generator and the runtime framework
39
implementation 'com.brightspot.graphql:graphql'
40
implementation 'com.google.inject:guice:5.1.0'
41
42
// Domain model and services
43
implementation project(':domain')
44
implementation project(':services')
45
}
46

Key Patterns Demonstrated

This example demonstrates all recommended implementation patterns:

  1. Type Mappings (CRITICAL): Money, Address, ProductImage types use static helper methods for consistent conversion across the application.

  2. Separate Implementation Classes (HIGH): Complex types like Category, Cart, and Order use dedicated implementation classes for better maintainability.

  3. Static Helper Methods (HIGH): toMoney(), toAddress(), toProductImage() are reusable static methods called from multiple places.

  4. Stream API (HIGH): Connection types and collections use Stream API for clean, functional transformations.

  5. Union Type Resolution: OrderResult union demonstrates error handling pattern with success and error types.

  6. Defensive Error Handling (REQUIRED): The placeOrder mutation 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

1
project/
2
├── graphql-schemas/
3
│ ├── public-api/
4
│ │ └── schema.graphql
5
│ ├── admin-api/
6
│ │ └── schema.graphql
7
│ └── internal-api/
8
│ └── schema.graphql
9
├── codegen/
10
│ ├── PublicAPICodeGenerator.java
11
│ ├── AdminAPICodeGenerator.java
12
│ └── InternalAPICodeGenerator.java
13
└── src/main/java/
14
└── com/example/
15
├── publicapi/
16
│ └── PublicAPISchemaContextImpl.java
17
├── adminapi/
18
│ └── AdminAPISchemaContextImpl.java
19
└── internalapi/
20
└── InternalAPISchemaContextImpl.java

Code Generator for Each Schema

1
package com.example.codegen;
2
3
import com.psddev.graphql.schema.codegen.GraphQLCodeGenerator;
4
import java.nio.file.Path;
5
6
public class PublicAPICodeGenerator {
7
public static void main(String[] args) {
8
Path projectRoot = Path.of("").toAbsolutePath();
9
10
new 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
}
1
package com.example.codegen;
2
3
import com.psddev.graphql.schema.codegen.GraphQLCodeGenerator;
4
import java.nio.file.Path;
5
6
public class AdminAPICodeGenerator {
7
public static void main(String[] args) {
8
Path projectRoot = Path.of("").toAbsolutePath();
9
10
new 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

1
sourceSets {
2
main {
3
java {
4
srcDir 'src/main/java'
5
srcDir 'build/generated/graphql/public'
6
srcDir 'build/generated/graphql/admin'
7
srcDir 'build/generated/graphql/internal'
8
}
9
}
10
}
11
12
tasks.register('generatePublicAPICode', JavaExec) {
13
classpath = sourceSets.main.runtimeClasspath
14
mainClass = 'com.example.codegen.PublicAPICodeGenerator'
15
inputs.files(fileTree('graphql-schemas/public-api'))
16
outputs.dir('build/generated/graphql/public')
17
}
18
19
tasks.register('generateAdminAPICode', JavaExec) {
20
classpath = sourceSets.main.runtimeClasspath
21
mainClass = 'com.example.codegen.AdminAPICodeGenerator'
22
inputs.files(fileTree('graphql-schemas/admin-api'))
23
outputs.dir('build/generated/graphql/admin')
24
}
25
26
tasks.register('generateInternalAPICode', JavaExec) {
27
classpath = sourceSets.main.runtimeClasspath
28
mainClass = 'com.example.codegen.InternalAPICodeGenerator'
29
inputs.files(fileTree('graphql-schemas/internal-api'))
30
outputs.dir('build/generated/graphql/internal')
31
}
32
33
tasks.register('generateAllGraphQLCode') {
34
dependsOn('generatePublicAPICode', 'generateAdminAPICode', 'generateInternalAPICode')
35
}
36
37
tasks.named('compileJava') {
38
dependsOn('generateAllGraphQLCode')
39
}
40

Batch Operations Example

Example showing how to implement batch loading and bulk operations efficiently.

Schema

1
type Query {
2
"""Get multiple users by IDs in a single query."""
3
users(ids: [ID!]!): [User!]!
4
5
"""Get posts with their authors efficiently batch-loaded."""
6
posts(limit: Int = 10): [Post!]!
7
}
8
9
type Mutation {
10
"""Create multiple posts in a single operation."""
11
createPosts(inputs: [CreatePostInput!]!): CreatePostsResult!
12
}
13
14
type User {
15
id: ID!
16
username: String!
17
email: String!
18
posts: [Post!]!
19
}
20
21
type Post {
22
id: ID!
23
title: String!
24
content: String!
25
author: User!
26
}
27
28
input CreatePostInput {
29
title: String!
30
content: String!
31
authorId: ID!
32
}
33
34
type CreatePostsResult {
35
posts: [Post!]!
36
errors: [CreatePostError!]!
37
}
38
39
type CreatePostError {
40
input: CreatePostInput!
41
message: String!
42
}

Implementation with DataLoader Pattern

1
// code-snippets/batch/BatchSchemaContextImpl.java
2
3
package com.example.batch;
4
5
import com.example.generated.batch.*;
6
import com.example.domain.*;
7
import com.example.service.*;
8
import java.util.*;
9
import java.util.stream.Collectors;
10
import java.util.concurrent.CompletableFuture;
11
12
public class BatchSchemaContextImpl extends BatchSchemaContext {
13
14
private final UserService userService;
15
private final PostService postService;
16
private final DataLoaderRegistry dataLoaderRegistry;
17
18
public BatchSchemaContextImpl(
19
UserService userService,
20
PostService postService,
21
DataLoaderRegistry dataLoaderRegistry) {
22
this.userService = userService;
23
this.postService = postService;
24
this.dataLoaderRegistry = dataLoaderRegistry;
25
}
26
27
@Override
28
public BatchQuery query() {
29
return new BatchQuery() {
30
@Override
31
public List<BatchUser> users(List<String> ids) {
32
// Batch load all users in a single database query
33
List<DomainUser> domainUsers = userService.findByIds(ids);
34
35
return domainUsers.stream()
36
.map(BatchSchemaContextImpl.this::toUser)
37
.collect(Collectors.toList());
38
}
39
40
@Override
41
public List<BatchPost> posts(Integer limit) {
42
List<DomainPost> domainPosts = postService.findRecent(limit != null ? limit : 10);
43
44
// Pre-load all authors in a single query to avoid N+1 problem
45
Set<String> authorIds = domainPosts.stream()
46
.map(DomainPost::getAuthorId)
47
.collect(Collectors.toSet());
48
49
Map<String, DomainUser> authorsById = userService.findByIds(new ArrayList<>(authorIds))
50
.stream()
51
.collect(Collectors.toMap(DomainUser::getId, u -> u));
52
53
return domainPosts.stream()
54
.map(post -> toPost(post, authorsById.get(post.getAuthorId())))
55
.collect(Collectors.toList());
56
}
57
};
58
}
59
60
@Override
61
public BatchMutation mutation() {
62
return new BatchMutation() {
63
@Override
64
public BatchCreatePostsResult createPosts(List<BatchCreatePostInput> inputs) {
65
List<BatchPost> createdPosts = new ArrayList<>();
66
List<BatchCreatePostError> errors = new ArrayList<>();
67
68
// Batch validation
69
for (BatchCreatePostInput input : inputs) {
70
try {
71
validatePostInput(input);
72
} catch (ValidationException e) {
73
errors.add(new BatchCreatePostError() {
74
@Override
75
public BatchCreatePostInput input() {
76
return input;
77
}
78
79
@Override
80
public String message() {
81
return e.getMessage();
82
}
83
});
84
}
85
}
86
87
// Only create valid posts in a single batch operation
88
if (errors.isEmpty() || createdPosts.size() > 0) {
89
List<BatchCreatePostInput> validInputs = inputs.stream()
90
.filter(input -> errors.stream().noneMatch(e -> e.input().equals(input)))
91
.collect(Collectors.toList());
92
93
List<DomainPost> domainPosts = postService.createBatch(
94
validInputs.stream()
95
.map(input -> new CreatePostRequest(
96
input.title(),
97
input.content(),
98
input.authorId()
99
))
100
.collect(Collectors.toList())
101
);
102
103
// Batch load all authors
104
Set<String> authorIds = domainPosts.stream()
105
.map(DomainPost::getAuthorId)
106
.collect(Collectors.toSet());
107
108
Map<String, DomainUser> authorsById = userService.findByIds(new ArrayList<>(authorIds))
109
.stream()
110
.collect(Collectors.toMap(DomainUser::getId, u -> u));
111
112
createdPosts = domainPosts.stream()
113
.map(post -> toPost(post, authorsById.get(post.getAuthorId())))
114
.collect(Collectors.toList());
115
}
116
117
List<BatchPost> finalCreatedPosts = createdPosts;
118
List<BatchCreatePostError> finalErrors = errors;
119
120
return new BatchCreatePostsResult() {
121
@Override
122
public List<BatchPost> posts() {
123
return finalCreatedPosts;
124
}
125
126
@Override
127
public List<BatchCreatePostError> errors() {
128
return finalErrors;
129
}
130
};
131
}
132
};
133
}
134
135
private BatchUser toUser(DomainUser domain) {
136
return new BatchUser() {
137
@Override
138
public String id() {
139
return domain.getId();
140
}
141
142
@Override
143
public String username() {
144
return domain.getUsername();
145
}
146
147
@Override
148
public String email() {
149
return domain.getEmail();
150
}
151
152
@Override
153
public List<BatchPost> posts() {
154
// Lazy load posts for this user
155
List<DomainPost> domainPosts = postService.findByAuthor(domain.getId());
156
return domainPosts.stream()
157
.map(post -> toPost(post, domain))
158
.collect(Collectors.toList());
159
}
160
};
161
}
162
163
private BatchPost toPost(DomainPost domainPost, DomainUser author) {
164
return new BatchPost() {
165
@Override
166
public String id() {
167
return domainPost.getId();
168
}
169
170
@Override
171
public String title() {
172
return domainPost.getTitle();
173
}
174
175
@Override
176
public String content() {
177
return domainPost.getContent();
178
}
179
180
@Override
181
public BatchUser author() {
182
return toUser(author);
183
}
184
};
185
}
186
187
private void validatePostInput(BatchCreatePostInput input) throws ValidationException {
188
if (input.title() == null || input.title().trim().isEmpty()) {
189
throw new ValidationException("Title is required");
190
}
191
if (input.content() == null || input.content().trim().isEmpty()) {
192
throw new ValidationException("Content is required");
193
}
194
if (input.authorId() == null) {
195
throw new ValidationException("Author ID is required");
196
}
197
if (!userService.exists(input.authorId())) {
198
throw new ValidationException("Author does not exist: " + input.authorId());
199
}
200
}
201
}

Key Optimization Patterns

  1. Batch Loading: Load multiple records in a single database query
  2. Pre-loading: Fetch related data upfront to avoid N+1 queries
  3. Batch Mutations: Create/update multiple records in one operation
  4. Batch Validation: Validate all inputs before processing any
  5. 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

1
scalar Instant
2
scalar LocalDate
3
scalar Duration
4
scalar UUID
5
scalar URL
6
scalar EmailAddress
7
scalar PhoneNumber
8
scalar JSON
9
10
type Query {
11
event(id: UUID!): Event
12
}
13
14
type Event {
15
id: UUID!
16
title: String!
17
startTime: Instant!
18
endTime: Instant!
19
duration: Duration!
20
location: URL
21
organizerEmail: EmailAddress!
22
organizerPhone: PhoneNumber
23
metadata: JSON
24
createdAt: Instant!
25
date: LocalDate!
26
}

Code Generator Configuration

1
package com.example.scalars.codegen;
2
3
import com.psddev.graphql.schema.codegen.GraphQLCodeGenerator;
4
import com.psddev.graphql.schema.types.json.JsonScalar;
5
import com.psddev.graphql.schema.types.text.UrlScalar;
6
import com.psddev.graphql.schema.types.time.DurationScalar;
7
import com.psddev.graphql.schema.types.time.InstantScalar;
8
import com.psddev.graphql.schema.types.time.LocalDateScalar;
9
import com.psddev.graphql.schema.types.uuid.UuidScalar;
10
import java.nio.file.Path;
11
12
public class ScalarCodeGenerator {
13
public static void main(String[] args) {
14
Path projectRoot = Path.of("").toAbsolutePath();
15
16
new 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.java
2
3
package com.example.scalars;
4
5
import com.example.generated.scalars.*;
6
import com.example.domain.DomainEvent;
7
import com.example.service.EventService;
8
import com.fasterxml.jackson.databind.JsonNode;
9
import java.time.*;
10
import java.net.URL;
11
import java.util.UUID;
12
13
public class ScalarSchemaContextImpl extends ScalarSchemaContext {
14
15
private final EventService eventService;
16
17
public ScalarSchemaContextImpl(EventService eventService) {
18
this.eventService = eventService;
19
}
20
21
@Override
22
public ScalarQuery query() {
23
return new ScalarQuery() {
24
@Override
25
public ScalarEvent event(UUID id) {
26
DomainEvent domain = eventService.findById(id);
27
return domain != null ? toEvent(domain) : null;
28
}
29
};
30
}
31
32
private ScalarEvent toEvent(DomainEvent domain) {
33
return new ScalarEvent() {
34
@Override
35
public UUID id() {
36
return domain.getId();
37
}
38
39
@Override
40
public String title() {
41
return domain.getTitle();
42
}
43
44
@Override
45
public Instant startTime() {
46
return domain.getStartTime();
47
}
48
49
@Override
50
public Instant endTime() {
51
return domain.getEndTime();
52
}
53
54
@Override
55
public Duration duration() {
56
return Duration.between(domain.getStartTime(), domain.getEndTime());
57
}
58
59
@Override
60
public URL location() {
61
try {
62
return domain.getLocation() != null ? new URL(domain.getLocation()) : null;
63
} catch (Exception e) {
64
return null;
65
}
66
}
67
68
@Override
69
public String organizerEmail() {
70
return domain.getOrganizerEmail();
71
}
72
73
@Override
74
public String organizerPhone() {
75
return domain.getOrganizerPhone();
76
}
77
78
@Override
79
public JsonNode metadata() {
80
return domain.getMetadata();
81
}
82
83
@Override
84
public Instant createdAt() {
85
return domain.getCreatedAt();
86
}
87
88
@Override
89
public LocalDate date() {
90
return domain.getStartTime().atZone(ZoneId.systemDefault()).toLocalDate();
91
}
92
};
93
}
94
}

Testing Generated Code

Examples of how to test your GraphQL implementation.

Unit Test Example

1
package com.example.test;
2
3
import com.example.generated.blog.*;
4
import com.example.blog.BlogSchemaContextImpl;
5
import com.example.domain.*;
6
import com.example.service.*;
7
import org.junit.jupiter.api.BeforeEach;
8
import org.junit.jupiter.api.Test;
9
import org.mockito.Mock;
10
import org.mockito.MockitoAnnotations;
11
12
import java.time.Instant;
13
import java.util.Arrays;
14
import java.util.List;
15
16
import static org.junit.jupiter.api.Assertions.*;
17
import static org.mockito.Mockito.*;
18
19
class BlogSchemaContextImplTest {
20
21
@Mock
22
private PostService postService;
23
24
@Mock
25
private UserService userService;
26
27
private BlogSchemaContextImpl context;
28
29
@BeforeEach
30
void setUp() {
31
MockitoAnnotations.openMocks(this);
32
context = new BlogSchemaContextImpl(postService, userService);
33
}
34
35
@Test
36
void testGetPost() {
37
// Given
38
String postId = "post-123";
39
DomainPost domainPost = new DomainPost();
40
domainPost.setId(postId);
41
domainPost.setTitle("Test Post");
42
domainPost.setContent("Test content");
43
domainPost.setPublishedAt(Instant.now());
44
45
DomainUser author = new DomainUser();
46
author.setId("user-456");
47
author.setUsername("testuser");
48
author.setEmail("test@example.com");
49
domainPost.setAuthor(author);
50
51
when(postService.findById(postId)).thenReturn(domainPost);
52
53
// When
54
BlogQuery query = context.query();
55
BlogPost result = query.post(postId);
56
57
// Then
58
assertNotNull(result);
59
assertEquals(postId, result.id());
60
assertEquals("Test Post", result.title());
61
assertEquals("Test content", result.content());
62
assertNotNull(result.publishedAt());
63
assertNotNull(result.author());
64
assertEquals("testuser", result.author().username());
65
66
verify(postService).findById(postId);
67
}
68
69
@Test
70
void testGetPostNotFound() {
71
// Given
72
String postId = "nonexistent";
73
when(postService.findById(postId)).thenReturn(null);
74
75
// When
76
BlogQuery query = context.query();
77
BlogPost result = query.post(postId);
78
79
// Then
80
assertNull(result);
81
verify(postService).findById(postId);
82
}
83
84
@Test
85
void testGetAllPosts() {
86
// Given
87
DomainUser author = new DomainUser();
88
author.setId("user-456");
89
author.setUsername("testuser");
90
author.setEmail("test@example.com");
91
92
DomainPost post1 = new DomainPost();
93
post1.setId("post-1");
94
post1.setTitle("Post 1");
95
post1.setContent("Content 1");
96
post1.setAuthor(author);
97
post1.setPublishedAt(Instant.now());
98
99
DomainPost post2 = new DomainPost();
100
post2.setId("post-2");
101
post2.setTitle("Post 2");
102
post2.setContent("Content 2");
103
post2.setAuthor(author);
104
post2.setPublishedAt(Instant.now());
105
106
when(postService.findAll()).thenReturn(Arrays.asList(post1, post2));
107
108
// When
109
BlogQuery query = context.query();
110
List<BlogPost> results = query.allPosts();
111
112
// Then
113
assertNotNull(results);
114
assertEquals(2, results.size());
115
assertEquals("Post 1", results.get(0).title());
116
assertEquals("Post 2", results.get(1).title());
117
118
verify(postService).findAll();
119
}
120
121
@Test
122
void testCreatePost() {
123
// Given
124
BlogCreatePostInput input = new BlogCreatePostInput() {
125
@Override
126
public String title() {
127
return "New Post";
128
}
129
130
@Override
131
public String content() {
132
return "New content";
133
}
134
135
@Override
136
public String authorId() {
137
return "user-456";
138
}
139
};
140
141
DomainUser author = new DomainUser();
142
author.setId("user-456");
143
author.setUsername("testuser");
144
145
DomainPost createdPost = new DomainPost();
146
createdPost.setId("post-789");
147
createdPost.setTitle("New Post");
148
createdPost.setContent("New content");
149
createdPost.setAuthor(author);
150
createdPost.setPublishedAt(Instant.now());
151
152
when(postService.create(any())).thenReturn(createdPost);
153
154
// When
155
BlogMutation mutation = context.mutation();
156
BlogPost result = mutation.createPost(input);
157
158
// Then
159
assertNotNull(result);
160
assertEquals("post-789", result.id());
161
assertEquals("New Post", result.title());
162
assertEquals("New content", result.content());
163
164
verify(postService).create(any());
165
}
166
}

Integration Test Example

1
package com.example.test;
2
3
import com.example.blog.BlogSchemaContextImpl;
4
import com.example.generated.blog.BlogSchemaLoader;
5
import org.junit.jupiter.api.BeforeEach;
6
import org.junit.jupiter.api.Test;
7
import org.springframework.beans.factory.annotation.Autowired;
8
import org.springframework.boot.test.context.SpringBootTest;
9
import org.springframework.boot.test.web.client.TestRestTemplate;
10
import org.springframework.http.HttpEntity;
11
import org.springframework.http.HttpHeaders;
12
import org.springframework.http.MediaType;
13
14
import java.util.Map;
15
16
import static org.junit.jupiter.api.Assertions.*;
17
18
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
19
class GraphQLIntegrationTest {
20
21
@Autowired
22
private TestRestTemplate restTemplate;
23
24
@Test
25
void testQueryPost() {
26
// Given
27
String query = """
28
query {
29
post(id: "post-123") {
30
id
31
title
32
content
33
author {
34
username
35
email
36
}
37
}
38
}
39
""";
40
41
Map<String, Object> requestBody = Map.of("query", query);
42
43
HttpHeaders headers = new HttpHeaders();
44
headers.setContentType(MediaType.APPLICATION_JSON);
45
46
// When
47
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
48
var response = restTemplate.postForEntity("/graphql", request, Map.class);
49
50
// Then
51
assertEquals(200, response.getStatusCode().value());
52
assertNotNull(response.getBody());
53
54
Map<String, Object> data = (Map<String, Object>) response.getBody().get("data");
55
assertNotNull(data);
56
57
Map<String, Object> post = (Map<String, Object>) data.get("post");
58
assertEquals("post-123", post.get("id"));
59
assertEquals("Test Post", post.get("title"));
60
}
61
62
@Test
63
void testMutationCreatePost() {
64
// Given
65
String mutation = """
66
mutation {
67
createPost(input: {
68
title: "Integration Test Post"
69
content: "Content from integration test"
70
authorId: "user-456"
71
}) {
72
id
73
title
74
content
75
}
76
}
77
""";
78
79
Map<String, Object> requestBody = Map.of("query", mutation);
80
HttpHeaders headers = new HttpHeaders();
81
headers.setContentType(MediaType.APPLICATION_JSON);
82
83
// When
84
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
85
var response = restTemplate.postForEntity("/graphql", request, Map.class);
86
87
// Then
88
assertEquals(200, response.getStatusCode().value());
89
90
Map<String, Object> data = (Map<String, Object>) response.getBody().get("data");
91
Map<String, Object> post = (Map<String, Object>) data.get("createPost");
92
93
assertNotNull(post.get("id"));
94
assertEquals("Integration Test Post", post.get("title"));
95
assertEquals("Content from integration test", post.get("content"));
96
}
97
}

Performance Optimization Patterns

Pattern 1: Field-Level Caching

1
package com.example.performance;
2
3
import com.google.common.cache.Cache;
4
import com.google.common.cache.CacheBuilder;
5
import java.util.concurrent.TimeUnit;
6
7
public class CachingSchemaContext extends MySchemaContext {
8
9
private final Cache<String, MyProduct> productCache;
10
private final Cache<String, MyUser> userCache;
11
12
public CachingSchemaContext(ProductService productService, UserService userService) {
13
super(productService, userService);
14
15
// Configure caches with appropriate TTL and size limits
16
this.productCache = CacheBuilder.newBuilder()
17
.maximumSize(1000)
18
.expireAfterWrite(5, TimeUnit.MINUTES)
19
.build();
20
21
this.userCache = CacheBuilder.newBuilder()
22
.maximumSize(500)
23
.expireAfterWrite(10, TimeUnit.MINUTES)
24
.build();
25
}
26
27
@Override
28
protected MyProduct toProduct(DomainProduct domain) {
29
return productCache.get(domain.getId(), () -> super.toProduct(domain));
30
}
31
32
@Override
33
protected MyUser toUser(DomainUser domain) {
34
return userCache.get(domain.getId(), () -> super.toUser(domain));
35
}
36
37
public void invalidateProduct(String id) {
38
productCache.invalidate(id);
39
}
40
41
public void invalidateUser(String id) {
42
userCache.invalidate(id);
43
}
44
}

Pattern 2: Lazy Loading with Suppliers

1
package com.example.performance;
2
3
import java.util.function.Supplier;
4
import java.util.List;
5
import java.util.stream.Collectors;
6
7
public class LazyLoadingSchemaContext extends MySchemaContext {
8
9
/**
10
* Use Suppliers for expensive fields that might not always be accessed.
11
*/
12
private MyProduct toProductWithLazyLoading(DomainProduct domain) {
13
return new MyProduct() {
14
// Eager fields - always loaded
15
@Override
16
public String id() {
17
return domain.getId();
18
}
19
20
@Override
21
public String name() {
22
return domain.getName();
23
}
24
25
// Lazy field - only loaded if accessed
26
private Supplier<List<MyReview>> reviewsSupplier;
27
28
@Override
29
public List<MyReview> reviews() {
30
if (reviewsSupplier == null) {
31
reviewsSupplier = () -> {
32
List<DomainReview> domainReviews = reviewService.getReviews(domain.getId());
33
return domainReviews.stream()
34
.map(this::toReview)
35
.collect(Collectors.toList());
36
};
37
}
38
return reviewsSupplier.get();
39
}
40
41
// Lazy field with caching - loaded once if accessed
42
private List<MyRelatedProduct> relatedProducts;
43
44
@Override
45
public List<MyRelatedProduct> relatedProducts() {
46
if (relatedProducts == null) {
47
List<DomainProduct> related = productService.findRelated(domain.getId());
48
relatedProducts = related.stream()
49
.map(LazyLoadingSchemaContext.this::toProduct)
50
.collect(Collectors.toList());
51
}
52
return relatedProducts;
53
}
54
};
55
}
56
}

Pattern 3: Connection Cursor-Based Pagination

1
package com.example.performance;
2
3
import java.util.Base64;
4
import java.nio.charset.StandardCharsets;
5
import java.util.List;
6
import java.util.stream.Collectors;
7
import java.util.stream.IntStream;
8
9
public class PaginationSchemaContext extends MySchemaContext {
10
11
@Override
12
public MyQuery query() {
13
return new MyQuery() {
14
@Override
15
public MyProductConnection products(Integer first, String after, Integer last, String before) {
16
// Decode cursors
17
Integer afterOffset = after != null ? decodeCursor(after) : null;
18
Integer beforeOffset = before != null ? decodeCursor(before) : null;
19
20
// Calculate offset and limit
21
int offset = afterOffset != null ? afterOffset + 1 : 0;
22
int limit = first != null ? first : (last != null ? last : 20);
23
24
// Fetch from database with offset and limit
25
PaginatedResult<DomainProduct> result = productService.findPaginated(offset, limit);
26
27
return new MyProductConnection() {
28
@Override
29
public List<MyProductEdge> edges() {
30
List<DomainProduct> products = result.getItems();
31
return IntStream.range(0, products.size())
32
.mapToObj(index -> {
33
DomainProduct product = products.get(index);
34
return new MyProductEdge() {
35
@Override
36
public MyProduct node() {
37
return toProduct(product);
38
}
39
40
@Override
41
public String cursor() {
42
return encodeCursor(offset + index);
43
}
44
};
45
})
46
.collect(Collectors.toList());
47
}
48
49
@Override
50
public MyPageInfo pageInfo() {
51
return new MyPageInfo() {
52
@Override
53
public Boolean hasNextPage() {
54
return result.hasMore();
55
}
56
57
@Override
58
public Boolean hasPreviousPage() {
59
return offset > 0;
60
}
61
62
@Override
63
public String startCursor() {
64
return result.isEmpty() ? null : encodeCursor(offset);
65
}
66
67
@Override
68
public String endCursor() {
69
return result.isEmpty() ? null
70
: encodeCursor(offset + result.getItems().size() - 1);
71
}
72
};
73
}
74
75
@Override
76
public Integer totalCount() {
77
// Optional: Cache this or make it optional in schema
78
return productService.count();
79
}
80
};
81
}
82
};
83
}
84
85
private String encodeCursor(int offset) {
86
return Base64.getEncoder().encodeToString(
87
String.valueOf(offset).getBytes(StandardCharsets.UTF_8)
88
);
89
}
90
91
private Integer decodeCursor(String cursor) {
92
try {
93
byte[] decoded = Base64.getDecoder().decode(cursor);
94
return Integer.parseInt(new String(decoded, StandardCharsets.UTF_8));
95
} catch (Exception e) {
96
throw new IllegalArgumentException("Invalid cursor: " + cursor);
97
}
98
}
99
}

Summary

These advanced examples demonstrate:

  1. E-commerce API: Complete production-ready implementation with all recommended patterns
  2. Multi-Schema Projects: Managing multiple APIs in one codebase
  3. Batch Operations: Efficient bulk loading and mutations
  4. Custom Scalars: Working with extended type systems
  5. Testing: Unit and integration test patterns
  6. Performance: Caching, lazy loading, and pagination strategies

Use these patterns as reference implementations for your own GraphQL APIs.