Spring HATEOAS vs. hateoflux

This section provides a side-by-side comparison of hateoflux and Spring HATEOAS, showcasing areas where each library excels, aligns, or differs. By exploring key differences, unique advantages, and unsupported features, this guide helps you make informed decisions for building hypermedia APIs in reactive Spring applications.

  1. Representation Models
    1. Spring HATEOAS Approach
    2. hateoflux Approach
  2. Response Handling
    1. Spring HATEOAS Approach
    2. hateoflux Approach
  3. Assemblers and Boilerplate Code
    1. Spring HATEOAS Approach
    2. hateoflux Approach
  4. Pagination Handling
    1. Spring HATEOAS Approach
    2. hateoflux Approach
  5. Representation Model Processors
    1. Spring HATEOAS Approach
    2. hateoflux Approach
  6. Resource Naming
  7. Handling Collections
  8. Type Safety and Link Building
  9. Documentation and WebFlux Support
  10. Media Types
  11. Affordance
  12. CURIE Support
  13. Summary Table of Features

For a TL;DR, head to the summary table at the bottom.

Representation Models

In hypermedia-driven APIs, the way resources are represented and structured is fundamental to ensuring seamless data interaction between the server and client. Spring HATEOAS and hateoflux adopt different approaches to handling these representations, with Spring utilizing “Models” and hateoflux employing “Wrappers.” This distinction is pivotal in understanding how each framework manages resource enrichment and hypermedia controls.

Spring HATEOAS Approach

Spring HATEOAS leverages representation models to encapsulate resources along with their hypermedia links. The primary classes used are RepresentationModel for individual resources and CollectionModel for collections of resources. Developers often wrap or sometimes extend these classes to include additional attributes and links, facilitating a flexible and extensible way to build resource representations.

For example, an OrderModel might be defined as follows:

public class OrderModel extends RepresentationModel<OrderModel> {
    private String orderId;
    private String status;
    
    // Getters and setters
}

By extending RepresentationModel, the OrderModel inherently gains the ability to hold hypermedia links. However, it is still necessary to include all the fields of the presumably existing Order class. Additionally, if we want to add an _embedded resource, there is no easy way to do so unless we manually add it to the OrderModel.

To be fair, however, wrapping is very common in Spring, so the above example is not necessarily the norm. The problem with putting an _embedded resource, however, persists.

hateoflux Approach

In contrast, hateoflux utilizes wrappers to enhance domain objects with hypermedia links and embedded resources exclusively. The key classes provided by hateoflux include HalResourceWrapper, HalListWrapper. These wrappers are designed to be composed around existing domain models instead of inherited from them, ensuring that the original models remain clean and decoupled from hypermedia concerns.

For instance, an Order can be wrapped using HalResourceWrapper as shown below:

HalResourceWrapper<Order, Void> wrap = HalResourceWrapper.wrap(order);

By using wrappers, hateoflux maintains a clear separation between the domain models and their hypermedia representations. The concept of main and embedded resources is seamlessly integrated into the wrapping process. The ResourceT type represents the primary resource, while the EmbeddedT denotes any secondary resources that are embedded within the main resource. When no embedded resource is required, EmbeddedT is set to Void.

For example, when an order includes shipment details, the wrapper would encapsulate both the Order and the Shipment as an embedded resource:

{
  "id": 12345,
  "status": "Shipped",
  "_embedded": {
    "shipment": {
      "id": 98765,
      "carrier": "UPS",
      "trackingNumber": "1Z999AA10123456784",
      "status": "In Transit"
    }
  },
  "_links": {
    "self": { "href": "http://localhost/orders/12345" }
  }
}

See also:

Response Handling

Spring HATEOAS and hateoflux take significantly different approaches to handling responses in reactive applications, particularly regarding how they manage the challenges of HTTP responses in a reactive context.

Spring HATEOAS Approach

Spring HATEOAS relies on the standard Spring WebFlux ResponseEntity, requiring manual wrapping of resources:

  • Basic Response Structure: Uses standard ResponseEntity wrapped in Mono or containing Flux
  • Resource Wrapping: Manual wrapping of resources in EntityModel or CollectionModel
  • Reactive Challenges: Developers must handle the complexities of when to finalize headers and status codes in streaming scenarios
  • Type Safety: Complex nested generics when combining Mono/Flux with ResponseEntity and EntityModel

Example in Spring HATEOAS:

@GetMapping
public Mono<ResponseEntity<CollectionModel<EntityModel<Product>>>> getProducts() {
    return productService.findAll()
        .map(product -> EntityModel.of(product))
        .collectList()
        .map(products -> CollectionModel.of(products))
        .map(wrapped -> ResponseEntity.ok()
            .header("Custom-Header", "value")
            .body(wrapped));
}

hateoflux Approach

hateoflux provides dedicated reactive response types that handle the complexities of reactive HTTP responses:

  • Specialized Response Types:
    • HalResourceResponse for single resources
    • HalMultiResourceResponse for streaming multiple resources
    • HalListResponse for collections as a single HAL document
  • Fluent Header API: Similar to Spring, provides a fluent interface for header manipulation
  • Status Code Handling: Like Spring, provides factory methods for common HTTP status codes
  • Automatic Resource Wrapping: Seamless integration with HAL wrappers
  • Reactive-First Design: Purpose-built for reactive flows, handling the complexities of timing for headers and status codes
  • Reduced Boilerplate: Simplified method signatures while maintaining type safety

Example in hateoflux:

@GetMapping
public HalListResponse<Product, Void> getProducts(ServerWebExchange exchange) {
    return HalListResponse.ok(
        productAssembler.wrapInListWrapper(
            productService.findAll(), 
            exchange
        )
    ).withHeader("Custom-Header", "value");
}

Assemblers and Boilerplate Code

Spring HATEOAS Approach

Spring HATEOAS provides reactive assemblers like ReactiveRepresentationModelAssembler to help assemble resources in reactive applications. However, these assemblers are more skeletons than actual helpful Assemblers as opposed to their non-reactive counterparts. Implementing them is therefore verbose and may require additional boilerplate code. Developers need to manually create EntityModel instances and add links for each resource, which can clutter the code.

An implementation of an assembler might look like this:

@Component
public class ProductAssembler implements ReactiveRepresentationModelAssembler<Product, EntityModel<Product>> {

    @Override
    public Mono<EntityModel<Product>> toModel(Product product, ServerWebExchange exchange) {
        return WebFluxLinkBuilder.linkTo(
                        WebFluxLinkBuilder.methodOn(ProductController.class).getProduct(product.getId()), exchange)
                .withSelfRel()
                .toMono()
                .map(link -> EntityModel.of(product).add(link));
    }
}

This looks acceptable until a more complex structure is needed, where other resources might be required. Given the example above, we might want to embed a ShipmentDetail. Spring unfortunately does not help you here, and the embedding needs to be done manually in the Product class. In general, we see:

  • Verbosity: Requires manual resource wrapping and link addition in assemblers.
  • Boilerplate Code: Repetitive patterns for essentially the same logic that need to be implemented in every assembler. This increases maintenance overhead.

hateoflux Approach

In contrast, hateoflux’s assemblers provide built-in methods to wrap a single or a list of resources. The only thing to implement is the part that is different from one resource to another: the links. The following is an implementation of FlatHalWrapperAssembler. The end result is the same as above:

import de.kamillionlabs.hateoflux.linkbuilder.SpringControllerLinkBuilder;

@Component
public class ProductAssembler implements FlatHalWrapperAssembler<Product> {

    @Override
    public Link buildSelfLinkForResource(Product product, ServerWebExchange exchange) {
        return SpringControllerLinkBuilder
                .linkTo(ProductController.class, controller -> controller.getProduct(product.getId()))
                .prependBaseUrl(exchange)
                .withSelfRel();
    }

    @Override
    Link buildSelfLinkForResourceList(ServerWebExchange exchange) {
        //Also needs to be implemented
    }
    
    @Override
    public Class<Product> getResourceTClass() {                                                           
        return Product.class;                                                                             
    }
}

This appears similar to Spring; however, the important difference is that no “Models” or “Wrappers” are created. Instead, the self link for any Product or a list of them is defined. The wrapping itself is managed by the assembler. Usage in a controller is then as simple as:

@Autowired
ProductAssembler productAssembler;

@GetMapping("/{id}")
public Mono<HalResourceWrapper<Product, Void>> getProduct(@PathVariable String id, ServerWebExchange exchange) {
    Mono<Product> product = productService.findById(id);
    return productAssembler.wrapInResourceWrapper(product, exchange);
}

If a situation arises where an embedded resource (e.g., ShipmentDetail as above) is required, the EmbeddingHalWrapperAssembler can be implemented. Here as well, only the logic for creating the self links needs to be provided:

import de.kamillionlabs.hateoflux.linkbuilder.SpringControllerLinkBuilder;

@Component
public class ProductAssembler implements EmbeddingHalWrapperAssembler<Product, ShipmentDetail> {

    @Override
    public Link buildSelfLinkForResource(Product product, ServerWebExchange exchange) {
        return SpringControllerLinkBuilder
                .linkTo(ProductController.class, controller -> controller.getProduct(product.getId()))
                .prependBaseUrl(exchange)
                .withSelfRel();
    }

    @Override
    Link buildSelfLinkForEmbedded(ShipmentDetail shipmentDetail, ServerWebExchange exchange) {
        return Link.of("/shipment").slash(shipmentDetail.getId())
                .prependBaseUrl(exchange)
                .withSelfRel();
    }
    
    //Also implement getResourceTClass() and getEmbeddedTClass()                                                           
}

The usage remains straightforward:

@Autowired
ProductAssembler productAssembler;

@GetMapping("/{id}")
public Mono<HalResourceWrapper<Product, ShipmentDetail>> getProduct(@PathVariable String id,
                                                                    ServerWebExchange exchange) {
    Mono<Product> product = productService.findById(id);
    Mono<ShipmentDetail> shipment = shipmentService.findByProductId(id);
    return productAssembler.wrapInResourceWrapper(product, shipment, exchange);
}

As demonstrated, adding an embedded resource (or a list of them if required) is accomplished with a simple one-liner.

See also:

Pagination Handling

Spring HATEOAS Approach

In Spring MVC, the PagedResourcesAssembler helps create PagedModel instances with pagination metadata and navigation links (prev, next). However, in reactive environments using WebFlux and R2DBC, paging support is limited:

  • Repositories Return Flux<T>: With R2DBC, repositories return Flux<T> instead of Page<T>.
  • No Native Paging Support: The lack of Page<T> means tools like PagedResourcesAssembler are not available in WebFlux.
  • Manual Implementation Required: Developers must manually assemble pagination metadata and navigation links.

hateoflux Approach

Hateoflux simplifies pagination in reactive environments by providing HalListWrapper, which encapsulate pagination metadata and handle navigation links automatically.

Given we implemented the ProductAssembler i.e. simply overriding how and what links to create for a single and a list of Products, adding pagination can be added as simple as below:

import static de.kamillionlabs.hateoflux.utility.SortDirection.ASCENDING;
import static de.kamillionlabs.hateoflux.utility.SortDirection.DESCENDING;
import org.springframework.data.domain.Pageable;

@GetMapping
public Mono<HalListWrapper<Product, Void>> getProducts(Pageable pageable,
                                                       ServerWebExchange exchange) {
    //Note: Pageable is not required!
    int pageSize = pageable.getPageSize();
    long offset = pageable.getOffset();
    List<SortCriteria> sortCriteria = pageable.getSort().get()
            .map(o -> SortCriteria.by(o.getProperty(), o.getDirection().isAscending() ? ASCENDING : DESCENDING))
            .toList();

    Flux<Product> productFlux = productService.findAll(pageSize, pageNumber);
    Mono<Long> totalElements = productService.countAll();

    return productAssembler.wrapInListWrapper(productFlux, totalElements, pageSize, pageNumber, sortCriteria, exchange);
}

The wrapInListWrapper will automatically append configured links in the assembler (like self link and others if specified), add paging information, and add the navigation links next, prev, first, and last.

See also:

Representation Model Processors

Spring HATEOAS provides Representation Model Processors allowing developers to adjust hypermedia responses globally or conditionally. In contrast, hateoflux incorporates this functionality within its assemblers, combining the responsibilities of both assemblers and processors from Spring HATEOAS into a single, cohesive unit.

Spring HATEOAS Approach

With Spring HATEOAS, RepresentationModelProcessor allows modifications to be applied to a resource representation post-assembly. This approach ensures that the logic for adding additional links or modifying the representation can be handled without modifying the core controller or assembler.

Suppose an Order resource could have a link added to initiate payment without embedding the logic in the OrderController or OrderAssembler. The implementation could look like so:

@Component
public class PaymentProcessor implements RepresentationModelProcessor<EntityModel<Order>> {

    @Override
    public EntityModel<Order> process(EntityModel<Order> model) {
        Order order = model.getContent();
        if (order != null && "AWAITING_PAYMENT".equals(order.getState())) {
            model.add(Link.of("/payments/{orderId}")
                    .withRel(LinkRelation.of("payments")) 
                            .expand(model.getContent().getOrderId()));
        }
        return model;
    }
}

The processor is executed automatically by spring effectively postprocessing every EntitiyModel<Order>. It is therefore not required to change the controller or the assembler class.

hateoflux Approach

Assemblers in hateoflux are used to encapsulate the creation of resource representations and the addition of hypermedia links, combining the functionality of both Spring HATEOAS assemblers and processors. This consolidated approach allows link-building logic and resource modifications to be centralized within the assembler, simplifying the development process.

To mimic the functionality of Spring described above, an OrderAssembler would look like the following:

@Component
public class OrderAssembler implements FlatHalWrapperAssembler<Order> {

    @Override
    public Link buildSelfLinkForResource(Order order, ServerWebExchange exchange) {
        return Link.of("/orders/" + order.getOrderId())
                   .withSelfRel()
                   .prependBaseUrl(exchange);
    }

    @Override
    public List<Link> buildOtherLinksForResource(Order order, ServerWebExchange exchange) {
        List<Link> links = new ArrayList<>();

        // A payment link is added if the order is awaiting payment
        if ("AWAITING_PAYMENT".equals(order.getState())) {
            links.add(Link.of("/payments/" + order.getOrderId())
                          .withRel("payment")
                          .prependBaseUrl(exchange));
        }

        return links;
    }

    @Override
    public Link buildSelfLinkForResourceList(ServerWebExchange exchange) {
        return Link.of("/orders")
                   .withSelfRel()
                   .prependBaseUrl(exchange);
    }
}

The assembler handles both the wrapping of resources and the addition of other links. The wrapping is done in the FlatHalWrapperAssembler and does not need to be reimplemented.

Similar to Spring, the controller stays unchanged by the types of links added.

Resource Naming

Both Spring HATEOAS and hateoflux use the @Relation annotation with the same semantics, allowing developers to customize serialization names of resources.

This annotation controls how resources and collections are named in the serialized output, ensuring consistency with domain terminology.

Example:

@Relation(itemRelation = "book", collectionRelation = "books")
public class BookDTO {
    private String title;
    private String author;
    // Getters and setters
}

Serialized Output:

{
  "_embedded": {
    "books": [
      {
        "title": "Effective Java",
        "author": "Joshua Bloch"
      }
    ]
  }
}

In conclusion, there is no difference.

Handling Collections

When assembling collections of resources, Spring HATEOAS requires collecting a Flux into a List using collectList(). This is necessary for creating CollectionModel instances but may have performance implications with large datasets due to loading all elements into memory. See e.g. the default implementation in the ReactiveRepresentationModelAssembler:

 default Mono<CollectionModel<D>> toCollectionModel(Flux<? extends T> entities, ServerWebExchange exchange) {
    return entities.flatMap((entity) -> {
        return this.toModel(entity, exchange);
    }).collectList() // <---- here
            .map(CollectionModel::of);
}

This is a necessary evil, or rather a trade-off, for being able to gather multiple resources together and assign information to them. In hateoflux the pagination or linking for a given list of resources, relies also on collectList().

It is important to be mindful of the performance implications that collectList() may have. Returning lists or pages should be done with caution.

Spring HATEOAS provides type-safe link building using WebFluxLinkBuilders methodOn() and linkTo(). It effectively handles path variables and query parameters. hateoflux provides a similar option and is also able to distinguish between query and path variables.

Example usage in Spring HATEOAS:

Mono<Link> selfLink = WebFluxLinkBuilder.linkTo(
        WebFluxLinkBuilder.methodOn(UserController.class).getUser(id))
        .withSelfRel()
        .toMono();

Example usage in hateoflux:

import de.kamillionlabs.hateoflux.linkbuilder.SpringControllerLinkBuilder;

Link selfLink = SpringControllerLinkBuilder.linkTo(
        UserController.class, controller -> controller.getUser(id))
        .withSelfRel();

Note that hateoflux provides a Link immediately whereas Spring’s WebFluxLinkBuilder is only able to return a Mono<Link>. If toMono() is not called, the process remains in the builder.

See also:

Documentation and WebFlux Support

Spring HATEOAS was primarily developed for Spring MVC applications. Although it does provide support for Spring WebFlux, the documentation and examples are more comprehensive for MVC, which can pose challenges for developers building hypermedia-driven APIs in reactive environments.

In contrast, hateoflux is specifically designed for reactive applications using Spring WebFlux. It offers targeted documentation and examples tailored to reactive programming, making the development process more straightforward. Additionally, hateoflux does not carry the extensive baggage that Spring HATEOAS has, making it smaller and lighter both in size and in terms of ease of use, overview, and comprehension.

Advantages:

  • Reactive-First Design: Optimized for reactive applications.
  • Focused Documentation: Comprehensive guidance for WebFlux reduces the learning curve.
  • Enhanced Developer Experience: Simplifies implementation in reactive environments.
  • Lightweight Framework: Smaller in size and easier to understand compared to Spring HATEOAS.

Media Types

TL;DR hateoflux only supports HAL+JSON

A significant distinction between Spring HATEOAS and hateoflux is their support for various media types. Spring HATEOAS offers extensive support for multiple media types, including HAL, Collection+JSON, and others. This flexibility allows developers to cater to diverse client requirements and standards, enabling richer interactions and broader compatibility across different API consumers. However, supporting multiple media types introduces additional complexity and abstraction layers, which can make the implementation more cumbersome and harder to maintain.

In contrast, hateoflux is intentionally designed to support only HAL+JSON. This focused approach aligns with hateoflux’s goal of being a small, concise library that covers the most essential features needed for building hypermedia-driven APIs in reactive environments. By limiting support to HAL+JSON, hateoflux simplifies the development process, offering a more straightforward and direct integration with Spring WebFlux and existing Spring projects. This design choice reduces overhead and makes the library easier to use for developers who primarily require HAL+JSON for their APIs.

While hateoflux excels in providing a streamlined and efficient solution for HAL+JSON-based hypermedia APIs, Spring HATEOAS retains an edge in scenarios where support for multiple media types is essential. Developers needing advanced hypermedia features and broader media type compatibility might prefer Spring HATEOAS despite its increased complexity. Conversely, for projects that prioritize simplicity, performance, and are content with HAL+JSON, hateoflux offers a more intuitive and maintainable alternative.

Affordance

TL;DR hateoflux does not supports affordance

Spring HATEOAS supports affordances, a powerful feature that allows developers to define actions that clients can perform on resources. By using affordances, developers can enrich API responses with metadata describing potential actions, such as form fields for data input or additional details on how a client might interact with a resource. This makes APIs more self-descriptive, enabling clients to discover possible operations dynamically without requiring hardcoded knowledge of the server’s capabilities. For example, affordances can define input parameters or constraints directly within the response, making it clear what actions a client is permitted to take.

In contrast, hateoflux does not support affordances, aligning with its goal of being a lightweight, simplified library that focuses on core hypermedia functionality. By omitting affordances, hateoflux remains compact and direct, prioritizing essential HATEOAS elements such as resource wrapping, link building, and pagination for HAL+JSON representations. This approach benefits developers who require a straightforward, low-overhead solution and can handle any additional action-specific metadata outside the API response itself, such as through separate documentation or client logic.

CURIE Support

TL;DR hateoflux does not supports CURIE

Spring HATEOAS includes support for CURIEs (Compact URIs), which enable the use of namespaced relation types in hypermedia representations. CURIEs simplify the representation of custom relation types, making APIs more readable and organized. This feature is valuable in complex APIs where consistent referencing of custom link relations is necessary, allowing for clearer client interpretation and easier navigation.

At present, hateoflux does not support CURIEs. While this is a beneficial feature for managing custom relation types, it has not been implemented in hateoflux as it has not been prioritized for current releases. Despite its absence, CURIE support is recognized as a useful enhancement, and it remains a potential addition for future versions of the library.

Summary Table of Features

The following table summarizes the main comparisons between Spring HATEOAS and hateoflux to highlight their strengths, limitations, and areas of alignment when building hypermedia-driven APIs in reactive Spring applications:

Aspect Spring HATEOAS (in WebFlux) hateoflux
Representation Models ⚠️
Uses wrappers and inheritance-based models. Embedding resources has to be done manually and by inheritance or creating distinct classes.

Uses wrappers for main and embedded resources, keeping domain models clean and decoupled.
Response Types ⚠️
No dedicated response types; relies on standard ResponseEntity with manual handling of reactive flows

Dedicated response types (HalResourceResponse, HalMultiResourceResponse, HalListResponse) optimized for reactive flows
Type Safety and Boilerplate Code
Complex nested generics and significant boilerplate when combining ResponseEntity, reactive types, and resources

Clean, type-safe interfaces with dedicated response types
Assemblers and Boilerplate Code
Verbose with manual resource wrapping and link addition; more boilerplate code needed.

Simplified with built-in methods; only links need to be specified in assemblers.
Pagination Handling
Limited or non-existing in reactive environments; requires manual implementation.

Easy pagination with HalListWrapper; handles metadata and navigation links automatically.
Representation Model Processors
Supports processors (RepresentationModelProcessor) to adjust hypermedia responses globally or conditionally.
⚠️
Functionality is incorporated within assemblers; processors are not separate.
Resource Naming
Uses @Relation for customizing serialization names.

Also uses @Relation with identical functionality; no difference.
Handling Collections ⚠️
Requires collectList(), which can impact performance with large datasets.
⚠️
Also requires collectList(), with similar performance implications.
Type Safety & Link Building
Type-safe link building using WebFluxLinkBuilder; links returned as Mono<Link>.

Type-safe link building; provides Link immediately without wrapping in Mono<Link>.
Documentation Support
Better for Spring MVC; less comprehensive for WebFlux, challenging for reactive development.

Tailored for reactive Spring WebFlux with focused documentation and examples.
Media Types
Supports multiple media types (HAL, Collection+JSON, etc.), offering flexibility.
⚠️
Only supports HAL+JSON; simpler but less flexible for clients needing other media types.
Affordance
Supports affordances, enabling clients to discover actions they can perform on resources.

Does not support affordances
CURIE Support
Supports CURIEs (Compact URIs) for namespaced relation types.

Does not support CURIEs
Framework Weight ⚠️
Heavier with more extensive features; may add complexity and overhead.

Lightweight and easier to use in reactive applications; focuses on essential features.