Skip to main content

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.

Was this page helpful?

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.