Skip to main content

Implementation Patterns

This guide covers best practices and patterns for implementing the code generated by the GraphQL Code Generator.

Pattern Selection Guide

Choose the right pattern based on your use case:

ScenarioPatternPriority
Existing domain model matches GraphQL typeType Mappings with @javaTypeCRITICAL - Use this first
Type has >5 fields or complex logicSeparate Implementation ClassHIGH
Creating multiple instances of same complex typeStatic Helper MethodHIGH
Transforming a collection to GraphQL typesStream APIHIGH
Simple type (≤3 fields, used once)Inline Anonymous ClassMEDIUM
Union or interface type resolutionAlways add defensive error handlingREQUIRED

Pattern 1: Type Mappings

Use when: You have existing domain models that map well to your GraphQL schema.

Why: Least code, single source of truth, automatic conversion by the framework.

Schema with @javaType directive:

1
"""Root query type for the blog API."""
2
type Query {
3
"""Get a post by ID."""
4
post(id: ID!): Post
5
6
"""Get all posts."""
7
allPosts: [Post!]!
8
9
"""Get a user by ID."""
10
user(id: ID!): User
11
}
12
13
"""Root mutation type."""
14
type Mutation {
15
"""Create a new post."""
16
createPost(input: CreatePostInput!): Post!
17
18
"""Update an existing post."""
19
updatePost(id: ID!, input: UpdatePostInput!): Post!
20
21
"""Delete a post."""
22
deletePost(id: ID!): Boolean!
23
}
24
25
"""A blog post."""
26
type Post @javaType(class: "com.example.domain.Post") {
27
id: ID!
28
title: String!
29
content: String!
30
author: User!
31
publishedAt: Instant
32
}
33
34
"""A user."""
35
type User @javaType(class: "com.example.domain.User") {
36
id: ID!
37
username: String!
38
email: String!
39
posts: [Post!]!
40
}
41
42
"""Input for creating a post."""
43
input CreatePostInput {
44
title: String!
45
content: String!
46
authorId: ID!
47
}
48
49
"""Input for updating a post."""
50
input UpdatePostInput {
51
title: String
52
content: String
53
}
54
55
"""ISO 8601 instant scalar."""
56
scalar Instant
57
58
"""Directive for mapping GraphQL types to Java classes."""
59
directive @javaType(
60
"""Fully qualified Java class name."""
61
class: String!
62
) on OBJECT | INTERFACE

Implementation:

1
package com.example.patterns;
2
3
import com.example.generated.*;
4
import com.example.domain.User;
5
import com.example.service.UserService;
6
7
public class TypeMappingExample extends MySchemaContext {
8
9
private final UserService userService;
10
11
public TypeMappingExample(UserService userService) {
12
this.userService = userService;
13
}
14
15
@Override
16
public MyQuery query() {
17
return new MyQuery() {
18
@Override
19
public User user(String id) {
20
return userService.findById(id);
21
}
22
};
23
}
24
25
@Override
26
protected MyUser toUser(User input) {
27
return new MyUser() {
28
@Override
29
public String id() {
30
return input.getId();
31
}
32
33
@Override
34
public String username() {
35
return input.getUsername();
36
}
37
38
@Override
39
public String email() {
40
return input.getEmail();
41
}
42
};
43
}
44
}

How It Works

  1. The @javaType(class: "...") directive (or an addTypeMapping builder call) tells the generator to use your domain class wherever that GraphQL type appears, so generated method signatures accept and return your domain objects directly
  2. For each mapped type, the generated schema context declares an abstract converter—protected abstract MyUser toUser(User input)—which you implement once
  3. At resolution time, the framework calls your converter to turn the domain object into the generated GraphQL model
  4. Conversion logic lives in exactly one place, instead of at every query and mutation site

Benefits

  • Query and mutation methods return domain objects directly (no conversion at the call site)
  • A single, generated-and-enforced conversion method per mapped type
  • Cleaner separation between domain model and GraphQL API
  • Reduces boilerplate significantly

When NOT to Use

  • When your domain model structure differs significantly from your GraphQL schema
  • When you need complex field-level transformations
  • When the domain model is mutable but you want immutable GraphQL types

Pattern 2: Separate Implementation Classes

Use when: Type has >5 fields OR contains complex field logic OR needs testing.

Why: Easier to test, maintain, and debug. Avoids deeply nested anonymous classes.

1
package com.example.patterns;
2
3
import com.example.generated.*;
4
import com.example.domain.DomainProduct;
5
import java.util.List;
6
7
// Separate implementation class for complex types
8
class ProductImpl extends MyProduct {
9
10
private final DomainProduct domain;
11
12
ProductImpl(DomainProduct domain) {
13
this.domain = domain;
14
}
15
16
@Override
17
public String id() {
18
return domain.getId();
19
}
20
21
@Override
22
public String name() {
23
return domain.getName();
24
}
25
26
@Override
27
public Double price() {
28
return domain.getPrice();
29
}
30
31
@Override
32
public String description() {
33
return domain.getDescription();
34
}
35
36
@Override
37
public MyCategory category() {
38
return new CategoryImpl(domain.getCategory());
39
}
40
41
@Override
42
public List<MyReview> reviews() {
43
return domain.getReviews().stream()
44
.map(ReviewImpl::new)
45
.toList();
46
}
47
48
@Override
49
public Integer stockQuantity() {
50
return domain.getInventory().getQuantity();
51
}
52
53
@Override
54
public Boolean inStock() {
55
return domain.getInventory().isInStock();
56
}
57
}

Benefits

  • Clean, testable implementation
  • Easy to debug and maintain
  • Avoids deep nesting
  • Can add helper methods
  • Easier to mock for testing

Example Usage in Schema Context

1
@Override
2
protected MyProduct toProduct(DomainProduct domain) {
3
return new ProductImpl(domain);
4
}

Pattern 3: Static Helper Methods

Use when: Creating multiple instances of the same complex type (e.g., in loops, collections).

Why: DRY principle, reusable, testable, keeps code clean.

1
package com.example.patterns;
2
3
import com.example.generated.*;
4
import com.example.domain.DomainStep;
5
import java.util.ArrayList;
6
import java.util.List;
7
8
public class StaticHelperExample extends MySchemaContext {
9
10
@Override
11
public MyQuery query() {
12
return new MyQuery() {
13
@Override
14
public MyWorkflow workflow(String id) {
15
DomainWorkflow domainWorkflow = workflowService.findById(id);
16
return toWorkflow(domainWorkflow);
17
}
18
};
19
}
20
21
private MyWorkflow toWorkflow(DomainWorkflow domain) {
22
return new MyWorkflow() {
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 List<MyStep> steps() {
35
List<MyStep> result = new ArrayList<>();
36
int index = 0;
37
38
// Use helper method in loop
39
for (DomainStep domainStep : domain.getSteps()) {
40
result.add(createStep(String.valueOf(index++), domainStep));
41
}
42
43
return result;
44
}
45
};
46
}
47
48
// Static helper method for reusable type creation
49
private static MyStep createStep(String id, DomainStep domainStep) {
50
return new MyStep() {
51
@Override
52
public String id() {
53
return id;
54
}
55
56
@Override
57
public String name() {
58
return domainStep.getName();
59
}
60
61
@Override
62
public String description() {
63
return domainStep.getDescription();
64
}
65
66
@Override
67
public Integer order() {
68
return domainStep.getOrder();
69
}
70
71
@Override
72
public Boolean required() {
73
return domainStep.isRequired();
74
}
75
};
76
}
77
}

Benefits

  • Avoids code duplication
  • Easy to test in isolation
  • Can be reused across multiple methods
  • Keeps the main implementation clean
  • Easier to maintain

When to Use

  • In loops where you create the same type multiple times
  • When the same conversion logic is needed in multiple places
  • For complex types that would clutter inline code

Pattern 4: Stream API for Collections

Use when: Transforming a collection to GraphQL types.

Why: Clean, functional, readable code.

1
package com.example.patterns;
2
3
import com.example.generated.*;
4
import com.example.domain.DomainProduct;
5
import java.util.List;
6
import java.util.stream.Collectors;
7
8
public class StreamAPIExample extends MySchemaContext {
9
10
@Override
11
public MyQuery query() {
12
return new MyQuery() {
13
@Override
14
public List<MyProduct> products() {
15
List<DomainProduct> domainProducts = productService.findAll();
16
17
// Use Stream API for clean collection transformations
18
return domainProducts.stream()
19
.map(StreamAPIExample.this::toProduct)
20
.collect(Collectors.toList());
21
}
22
23
@Override
24
public List<MyProduct> searchProducts(String query) {
25
return productService.search(query).stream()
26
.filter(p -> p.isActive())
27
.map(StreamAPIExample.this::toProduct)
28
.collect(Collectors.toList());
29
}
30
};
31
}
32
33
private MyProduct toProduct(DomainProduct domain) {
34
return new MyProduct() {
35
@Override
36
public String id() {
37
return domain.getId();
38
}
39
40
@Override
41
public String name() {
42
return domain.getName();
43
}
44
45
@Override
46
public Double price() {
47
return domain.getPrice();
48
}
49
};
50
}
51
}

Benefits

  • Readable and concise
  • Familiar to Java developers
  • Easy to chain with filters, maps, etc.
  • Encourages immutability
  • Better performance with parallel streams (when appropriate)

Common Stream Operations

1
// Filtering
2
domainProducts.stream()
3
.filter(p -> p.isActive())
4
.map(this::toProduct)
5
.collect(Collectors.toList());
6
7
// Mapping with index
8
IntStream.range(0, items.size())
9
.mapToObj(i -> createItem(String.valueOf(i), items.get(i)))
10
.collect(Collectors.toList());
11
12
// Grouping
13
domainProducts.stream()
14
.collect(Collectors.groupingBy(
15
p -> p.getCategory(),
16
Collectors.mapping(this::toProduct, Collectors.toList())
17
));

Pattern 5: Inline Anonymous Classes

Use when: Simple type (≤3 fields, used once).

Why: Quick and straightforward for simple cases.

1
package com.example.patterns;
2
3
import com.example.generated.*;
4
5
public class InlineAnonymousExample extends MySchemaContext {
6
7
@Override
8
public MyQuery query() {
9
return new MyQuery() {
10
@Override
11
public MyMetadata metadata() {
12
// Inline anonymous class for simple types
13
return new MyMetadata() {
14
@Override
15
public String version() {
16
return "1.0.0";
17
}
18
19
@Override
20
public String environment() {
21
return System.getenv("APP_ENV");
22
}
23
24
@Override
25
public Boolean maintenanceMode() {
26
return false;
27
}
28
};
29
}
30
};
31
}
32
}

When to Use

  • Simple types with 3 or fewer fields
  • Types used only once in your codebase
  • Static or configuration data
  • When the logic is trivial

When NOT to Use

  • Complex types with many fields
  • Types with complex field resolution logic
  • When you need to test the implementation in isolation
  • When the same type appears in multiple places

Complete Example: Blog API

This example demonstrates all recommended patterns working together in a realistic scenario.

Schema

1
"""Root query type for the blog API."""
2
type Query {
3
"""Get a post by ID."""
4
post(id: ID!): Post
5
6
"""Get all posts."""
7
allPosts: [Post!]!
8
9
"""Get a user by ID."""
10
user(id: ID!): User
11
}
12
13
"""Root mutation type."""
14
type Mutation {
15
"""Create a new post."""
16
createPost(input: CreatePostInput!): Post!
17
18
"""Update an existing post."""
19
updatePost(id: ID!, input: UpdatePostInput!): Post!
20
21
"""Delete a post."""
22
deletePost(id: ID!): Boolean!
23
}
24
25
"""A blog post."""
26
type Post @javaType(class: "com.example.domain.Post") {
27
id: ID!
28
title: String!
29
content: String!
30
author: User!
31
publishedAt: Instant
32
}
33
34
"""A user."""
35
type User @javaType(class: "com.example.domain.User") {
36
id: ID!
37
username: String!
38
email: String!
39
posts: [Post!]!
40
}
41
42
"""Input for creating a post."""
43
input CreatePostInput {
44
title: String!
45
content: String!
46
authorId: ID!
47
}
48
49
"""Input for updating a post."""
50
input UpdatePostInput {
51
title: String
52
content: String
53
}
54
55
"""ISO 8601 instant scalar."""
56
scalar Instant
57
58
"""Directive for mapping GraphQL types to Java classes."""
59
directive @javaType(
60
"""Fully qualified Java class name."""
61
class: String!
62
) on OBJECT | INTERFACE

Code Generator

1
package com.example.blog.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 BlogCodeGenerator {
8
public static void main(String[] args) {
9
Path projectRoot = Path.of("").toAbsolutePath();
10
11
new GraphQLCodeGenerator.Builder()
12
.sdlPath(projectRoot.resolve("src/main/resources/graphql/blog.graphql"))
13
.generatedSourcesOutputDir(projectRoot.resolve("build/generated/graphql"))
14
.generatedSourcesPackageName("com.example.blog.generated")
15
.classNamePrefix("Blog")
16
.addCustomScalar("Instant", InstantScalar.class)
17
.build()
18
.generate();
19
}
20
}

Implementation

1
package com.example.blog;
2
3
import com.example.blog.generated.*;
4
import com.example.domain.Post;
5
import com.example.domain.User;
6
import com.example.service.PostService;
7
import com.example.service.UserService;
8
import javax.inject.Inject;
9
import java.util.List;
10
11
public class BlogSchemaContextImpl extends BlogSchemaContext {
12
13
private final PostService postService;
14
private final UserService userService;
15
16
@Inject
17
public BlogSchemaContextImpl(PostService postService, UserService userService) {
18
this.postService = postService;
19
this.userService = userService;
20
}
21
22
@Override
23
public BlogQuery query() {
24
return new BlogQuery() {
25
@Override
26
public Post post(String id) {
27
return postService.findById(id);
28
}
29
30
@Override
31
public List<Post> allPosts() {
32
return postService.findAll();
33
}
34
35
@Override
36
public User user(String id) {
37
return userService.findById(id);
38
}
39
};
40
}
41
42
@Override
43
public BlogMutation mutation() {
44
return new BlogMutation() {
45
@Override
46
public Post createPost(BlogCreatePostInput input) {
47
return postService.create(
48
input.title(),
49
input.content(),
50
input.authorId()
51
);
52
}
53
54
@Override
55
public Post updatePost(String id, BlogUpdatePostInput input) {
56
return postService.update(id, input.title(), input.content());
57
}
58
59
@Override
60
public Boolean deletePost(String id) {
61
return postService.delete(id);
62
}
63
};
64
}
65
66
@Override
67
protected BlogPost toPost(Post input) {
68
return new BlogPost() {
69
@Override
70
public String id() {
71
return input.getId();
72
}
73
74
@Override
75
public String title() {
76
return input.getTitle();
77
}
78
79
@Override
80
public String content() {
81
return input.getContent();
82
}
83
84
@Override
85
public User author() {
86
return input.getAuthor();
87
}
88
89
@Override
90
public java.time.Instant publishedAt() {
91
return input.getPublishedAt();
92
}
93
};
94
}
95
96
@Override
97
protected BlogUser toUser(User input) {
98
return new BlogUser() {
99
@Override
100
public String id() {
101
return input.getId();
102
}
103
104
@Override
105
public String username() {
106
return input.getUsername();
107
}
108
109
@Override
110
public String email() {
111
return input.getEmail();
112
}
113
114
@Override
115
public List<Post> posts() {
116
return postService.findByAuthor(input.getId());
117
}
118
};
119
}
120
}

Key Patterns Used

  • Pattern 1: @javaType directive for type mappings (Post, User)
  • Pattern 4: Stream API would be used for collection transformations if needed
  • Pattern 5: Inline anonymous classes for query/mutation implementations
  • Pattern 1: Query methods return domain objects directly

Union and Interface Resolution

A GraphQL union becomes a generated Java interface (e.g. MySearchResult), and every member type's generated model class implements it (MyProduct, MyArticle). Resolution is automatic: the generated union type generator inspects which model class your returned object is an instance of—you never write a type resolver. Your job is simply to return instances of the generated models:

1
@Override
2
public MySearchResult search(String query) {
3
Object result = searchService.search(query);
4
5
if (result instanceof DomainProduct) {
6
return toProduct((DomainProduct) result); // a MyProduct, which implements MySearchResult
7
} else if (result instanceof DomainArticle) {
8
return toArticle((DomainArticle) result); // a MyArticle, which implements MySearchResult
9
} else if (result == null) {
10
return null;
11
} else {
12
throw new IllegalStateException(
13
"Unexpected search result type: " + result.getClass().getName());
14
}
15
}

Union Type Best Practices

  1. Always handle all possible types: Don't leave cases unhandled
  2. Add a defensive else clause: Throw an exception for unexpected types, with the type name in the message
  3. Return the generated models: Only instances of generated member classes resolve; arbitrary objects fail at runtime

Error Handling Patterns

Query Methods

1
@Override
2
public MyPost post(String id) {
3
DomainPost post = postService.findById(id);
4
5
// Return null for not found (GraphQL semantics)
6
if (post == null) {
7
return null;
8
}
9
10
return toPost(post);
11
}

Mutation Methods

1
@Override
2
public MyPost createPost(MyCreatePostInput input) {
3
try {
4
DomainPost post = postService.create(
5
input.title(),
6
input.content(),
7
input.authorId()
8
);
9
return toPost(post);
10
} catch (ValidationException e) {
11
// Let validation exceptions propagate to GraphQL errors
12
throw e;
13
} catch (Exception e) {
14
// Wrap unexpected exceptions with context
15
throw new RuntimeException(
16
"Failed to create post: " + e.getMessage(),
17
e
18
);
19
}
20
}

Performance Optimization

Lazy Loading

For expensive fields that might not be queried:

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
}

Caching

For frequently accessed data:

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
}

Pagination

For large result sets:

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
}

Testing Patterns

Unit Testing Schema Context

1
class MySchemaContextImplTest {
2
@Mock
3
private ProductService productService;
4
5
private MySchemaContextImpl context;
6
7
@BeforeEach
8
void setUp() {
9
MockitoAnnotations.openMocks(this);
10
context = new MySchemaContextImpl(productService);
11
}
12
13
@Test
14
void testGetProduct() {
15
// Given
16
DomainProduct domainProduct = new DomainProduct("1", "Test", 9.99);
17
when(productService.findById("1")).thenReturn(domainProduct);
18
19
// When
20
MyProduct result = context.query().product("1");
21
22
// Then
23
assertNotNull(result);
24
assertEquals("1", result.id());
25
assertEquals("Test", result.name());
26
verify(productService).findById("1");
27
}
28
}

Testing Separate Implementation Classes

1
class ProductImplTest {
2
@Test
3
void testProductImplementation() {
4
// Given
5
DomainProduct domain = new DomainProduct("1", "Test", 9.99);
6
7
// When
8
ProductImpl product = new ProductImpl(domain);
9
10
// Then
11
assertEquals("1", product.id());
12
assertEquals("Test", product.name());
13
assertEquals(9.99, product.price());
14
}
15
}

Best Practices Summary

  1. Type mappings with @javaType - Use this first for the biggest wins
  2. Separate implementation classes - Use for complex types
  3. Static helper methods - Use for reusable type creation
  4. Stream API - Use for collection transformations
  5. Inline anonymous classes - Only for simple, one-off types
  6. Defensive error handling - Always for union/interface resolution
  7. Lazy loading - For expensive fields
  8. Caching - For frequently accessed data
  9. Proper testing - Unit test your implementations

Next Steps

Was this page helpful?

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