Spring Boot
HAL serialization
custom controller
REST API
Java development

Enable HAL serialization in Spring Boot for custom controller method

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

HAL serialization in Spring Boot is straightforward for repository endpoints, but custom controller methods need explicit hypermedia types and resource wrappers so links are emitted correctly. A better pattern is to define the minimum successful flow first, make assumptions explicit, and only then optimize. This avoids brittle fixes and gives you a clear baseline when behavior changes under load or in different environments.

If a custom endpoint returns plain domain objects, clients receive JSON without _links, and HAL consumers lose discoverability. The fix is to return RepresentationModel or EntityModel/CollectionModel and to set content negotiation explicitly where needed. Treat configuration, runtime behavior, and validation as separate concerns. That separation helps you troubleshoot faster and gives teammates a stable mental model for ongoing maintenance.

Core Sections

1) Define the operating contract first

Before changing implementation details, write down the input shape, output guarantees, and failure behavior you expect. Include environment assumptions such as runtime version, network boundaries, data volume, and latency goals. This contract turns vague bugs into verifiable hypotheses. It also prevents accidental coupling between unrelated concerns, such as configuration and business logic. Teams that document these boundaries up front usually spend less time on regressions and more time on measurable improvements.

2) Return hypermedia resource types from custom endpoints

java
1@RestController
2@RequestMapping("/api/orders")
3@RequiredArgsConstructor
4class OrderController {
5
6  private final OrderService service;
7  private final OrderModelAssembler assembler;
8
9  @GetMapping(value = "/{id}", produces = "application/hal+json")
10  public EntityModel<OrderDto> byId(@PathVariable Long id) {
11    OrderDto dto = service.findById(id);
12    return assembler.toModel(dto);
13  }
14}

This baseline example is intentionally conservative. It favors clarity over cleverness and makes state transitions visible. Keep it running as a reference implementation while you iterate. If later optimization changes behavior, compare against this baseline to isolate the exact regression. In practice, this approach shortens debugging loops and keeps refactors from drifting away from expected behavior.

java
1@Component
2class OrderModelAssembler implements RepresentationModelAssembler<OrderDto, EntityModel<OrderDto>> {
3  @Override
4  public EntityModel<OrderDto> toModel(OrderDto dto) {
5    return EntityModel.of(
6      dto,
7      linkTo(methodOn(OrderController.class).byId(dto.id())).withSelfRel(),
8      linkTo(methodOn(OrderController.class).all()).withRel("orders")
9    );
10  }
11}
12
13@GetMapping(produces = "application/hal+json")
14public CollectionModel<EntityModel<OrderDto>> all() {
15  return CollectionModel.of(service.findAll().stream().map(assembler::toModel).toList());
16}

The second example adds operational hardening: better observability, explicit lifecycle handling, and safer defaults. Production systems fail at boundaries, not just in core logic, so edge-path behavior must be deliberate. Add logs or metrics at decision points, and prefer deterministic failure modes over silent fallbacks. That design makes on-call response significantly faster when incidents occur.

4) Validation and rollout strategy

Validate with Accept: application/hal+json, and assert _links shape in integration tests. Also verify clients that request application/json still get a compatible response if your API supports both media types. Keep a short regression checklist in your repository so every environment change can be verified consistently. Include success-path checks and one intentional failure case. Over time, this checklist becomes living documentation that protects future edits and keeps behavior stable across teams and release cycles.

Common Pitfalls

  • Returning plain DTOs and expecting HAL links to appear automatically.
  • Forgetting HAL media type negotiation and then debugging a non-hypermedia payload.
  • Building links inline in controllers, causing duplication and inconsistent relations.
  • Leaking internal entity models instead of API-facing DTOs.
  • Skipping integration tests for content negotiation and link presence.

Summary

Custom HAL endpoints are stable when you return Spring HATEOAS models, centralize link assembly, and verify negotiated media types in tests. The recurring pattern is simple: keep the core path explicit, add guardrails around it, and verify outcomes with repeatable tests before scaling complexity.


Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.