DEV Community

Cover image for Data Oriented Rest Api in Java
anand jaisy
anand jaisy

Posted on

Data Oriented Rest Api in Java

Data-oriented

Data-oriented programming promotes modeling information as immutable data and separating the business logic that operates on it. As the shift toward simpler, more focused programs has gained momentum, Java has introduced new features to better support this style—such as records for straightforward data modeling, sealed classes for representing fixed sets of alternatives, and pattern matching for flexible and expressive handling of polymorphic data.

Records, sealed classes, and pattern matching are complementary features in Java that collectively enhance support for data-oriented programming.

Records provide a concise way to model immutable data structures.
Sealed classes enable precise modeling of constrained hierarchies, allowing developers to define a fixed set of subclasses.
Pattern matching offers a type-safe and expressive mechanism for working with polymorphic data.
Java has introduced pattern matching in progressive stages:

Initially, only type-test patterns were supported in instanceof checks.
Subsequent enhancements extended support to switch statements, enabling more expressive control flow.
Most recently, deconstruction patterns for records were introduced in Java 19, allowing developers to extract and operate on the internal components of records in a streamlined, declarative manner.
These features together promote cleaner, more maintainable code by aligning data representation closely with the logic that operates on it.

Reference - https://www.infoq.com/articles/data-oriented-programming-java/

Building a REST Endpoint Using Java Sealed Interfaces and Records

In this article, we’ll build a REST endpoint using Java sealed interfaces and records to model success and failure outcomes in a clean, expressive, and type-safe way.

The same approach applies across popular Java frameworks such as Micronaut, Spring Boot, and Quarkus. For demonstration purposes, I’ll use Micronaut, but you can easily adapt the examples to the framework you’re most comfortable with.

Modeling Input and Output with Sealed Interfaces

We start by defining a common result type that represents either a successful outcome or a failure. Java’s sealed interfaces allow us to strictly control which implementations are permitted, while records provide concise, immutable data carriers.

public sealed interface IResult
        permits IResult.Success, IResult.Failure {

    record Success<T>(T value) implements IResult {}
    record Failure<T>(Exception error) implements IResult {}

    static <T> IResult success(T value) {
        return new Success<>(value);
    }

    static IResult failure(Exception ex) {
        return new Failure<>(ex);
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Success wraps a successful result
  • Failure wraps an exception
  • Static factory methods improve readability and remove the need for the new keyword at call sites
return IResult.failure(new ItemNotFoundException(ApplicationMessage.ValidationFieldMessage.RECORD_NOT_FOUND));
Enter fullscreen mode Exit fullscreen mode

Alternative: Without Static Factory Methods

public sealed interface IResult
        permits IResult.Success, IResult.Failure {

    record Success<T>(T value) implements IResult {}
    record Failure<T>(Exception error) implements IResult {}
}
Enter fullscreen mode Exit fullscreen mode
return new IResult.failure(new ItemNotFoundException(ApplicationMessage.ValidationFieldMessage.RECORD_NOT_FOUND));
Enter fullscreen mode Exit fullscreen mode

The only difference is the explicit use of the new keyword. Functionally, both approaches are equivalent.

Controller Implementation

Next, let’s look at how this result abstraction simplifies controller logic. By leveraging pattern matching with switch, we can map domain results directly to HTTP responses.

@Controller("/privacyPreference")
public class PrivacyPreferenceController implements IHttpAction<PrivacyPreferenceRecord.PrivacyPreferenceRequest, PrivacyPreferenceRequest, PrivacyPreferenceResponse> {
    private final IPrivacyPreferenceRepo<PrivacyPreferenceResponse, PrivacyPreferenceRequest, FilterSortRequest> iPrivacyPreferenceRepo;

    public PrivacyPreferenceController(IPrivacyPreferenceRepo<PrivacyPreferenceResponse, PrivacyPreferenceRequest, FilterSortRequest> iPrivacyPreferenceRepo) {
        this.iPrivacyPreferenceRepo = iPrivacyPreferenceRepo;
    }

    @Override
    public HttpResponse<?> find() {
        var result = this.iPrivacyPreferenceRepo.find();
        return switch (result) {
            case Success success -> HttpResponse.ok(success.value());
            case Failure failure -> HttpResponse.notFound(failure.error().getMessage());
        };
    }

    @Override
    public HttpResponse<?> get(@Nonnull UUID id) {
        var result = this.iPrivacyPreferenceRepo.get(id);
        return switch (result) {
            case Success success -> HttpResponse.ok(success.value());
            case Failure failure -> HttpResponse.notFound(failure.error().getMessage());
        };
    }

    @Override
    public HttpResponse<?> post(PrivacyPreferenceRecord.@Valid PrivacyPreferenceRequest request) {
        var result = this.iPrivacyPreferenceRepo.create(request);
        return switch (result) {
            case Success success -> HttpResponse.ok(success.value());
            case Failure failure -> HttpResponse.badRequest(failure.error().getMessage());
        };
    }

    @Override
    public HttpResponse<?> patch(@NotNull UUID id, PrivacyPreferenceRecord.PrivacyPreferenceRequest request) {
        var result = this.iPrivacyPreferenceRepo.update(id, request);
        return switch (result) {
            case Success success -> HttpResponse.ok(success.value());
            case Failure failure -> switch (failure.error()) {
                case DuplicateNameException e -> HttpResponse.badRequest(e.getMessage());
                case ItemNotFoundException e -> HttpResponse.notFound(e.getMessage());
                default -> HttpResponse.serverError(failure.error().getMessage());
            };
        };
    }

    @Override
    public HttpResponse<?> delete(@NotNull UUID id) {
        var result = this.iPrivacyPreferenceRepo.delete(id);
        return switch (result) {
            case Success _ -> HttpResponse.noContent();
            case Failure failure -> HttpResponse.badRequest(failure.error().getMessage());
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Why This Works Well

  • No exception-driven control flow in the controller
  • Compile-time safety via sealed types
  • Clear, readable HTTP mapping using pattern matching

Repository Contract

The repository interface consistently returns IResult, ensuring that all consumers handle success and failure explicitly.

public interface IPrivacyPreferenceRepo<R, T, C> {
    IResult find();
    IResult get(@NotNull UUID var1);
    IResult create(T var1);
    IResult update(@NotNull UUID var1, T var2);
    IResult delete(@NotNull UUID var1);
}
Enter fullscreen mode Exit fullscreen mode
@Singleton
public class PrivacyPreferenceRepository implements IPrivacyPreferenceRepo<PrivacyPreferenceResponse, PrivacyPreferenceRequest, FilterSortRequest> {
    private final Map<UUID, PrivacyPreference> privacyPreferenceMap;
    private final PrivacyPreferenceEntityMapper privacyPreferenceEntityMapper;
    private final PrivacyPreferenceResponseMapper privacyPreferenceResponseMapper;

    public PrivacyPreferenceRepository(RootProvider<Root> rootProvider, PrivacyPreferenceEntityMapper privacyPreferenceEntityMapper,
                                       PrivacyPreferenceResponseMapper privacyPreferenceResponseMapper) {
        this.privacyPreferenceMap = rootProvider.root().getPrivacyPreference();
        this.privacyPreferenceEntityMapper = privacyPreferenceEntityMapper;
        this.privacyPreferenceResponseMapper = privacyPreferenceResponseMapper;
    }

    @Override
    public IResult find() {
        return IResult.success(privacyPreferenceMap.values().stream()
                .map(privacyPreferenceResponseMapper)
                .toList());
    }

    @Override
    public IResult get(@NotNull UUID id) {
        PrivacyPreference privacyPreference = privacyPreferenceMap.get(id);
        if (privacyPreference == null)
            return IResult.failure(new ItemNotFoundException(ApplicationMessage.ValidationFieldMessage.RECORD_NOT_FOUND));

        return IResult.success(this.privacyPreferenceResponseMapper.apply(privacyPreference));
    }

    @Override
    public IResult create(PrivacyPreferenceRequest request) {
        if (privacyPreferenceMap.values().stream().anyMatch(x -> x.userId().equals(request.userId())))
            return IResult.failure(new DuplicateNameException(ApplicationMessage.ApplicationError.RECORD_ALREADY_EXISTS));

        var privacyPreference = this.privacyPreferenceEntityMapper.apply(request, Optional.empty());
        this.save(this.privacyPreferenceMap, privacyPreference);
        return IResult.success(privacyPreferenceResponseMapper.apply(privacyPreference));
    }

    @Override
    public IResult update(@NotNull UUID id, PrivacyPreferenceRequest request) {
        PrivacyPreference privacyPreference = privacyPreferenceMap.get(id);
        if (privacyPreference == null)
            return IResult.failure(new ItemNotFoundException(ApplicationMessage.ValidationFieldMessage.RECORD_NOT_FOUND));

        var updatePrivacyPreference =  this.privacyPreferenceEntityMapper.apply(request, Optional.of(privacyPreference));
        save(this.privacyPreferenceMap, updatePrivacyPreference);
        return IResult.success(privacyPreferenceResponseMapper.apply(updatePrivacyPreference));
    }

    @Override
    public IResult delete(@NotNull UUID id) {
        try {
            deleteCourseById(this.privacyPreferenceMap, id);
        } catch (Exception e) {
            return IResult.failure(new ItemNotFoundException(ApplicationMessage.AdditionalRequirementMessage.ADDITIONAL_REQUIREMENT_NOTFOUND));
        }
        return IResult.success(true);
    }

    @StoreParams("additionalRequirement")
    protected void save(Map<UUID, PrivacyPreference> additionalRequirement, @NonNull PrivacyPreference request) {
        additionalRequirement.put(request.id(), request);
    }

    @StoreParams("additionalRequirement")
    protected void deleteCourseById(Map<UUID, PrivacyPreference> additionalRequirement,@NonNull UUID id) {
        if (additionalRequirement.get(id) == null)
            throw new ItemNotFoundException(ApplicationMessage.AdditionalRequirementMessage.ADDITIONAL_REQUIREMENT_NOTFOUND);
        additionalRequirement.remove(id);
    }
}
Enter fullscreen mode Exit fullscreen mode

Alternative Functional style

@Serdeable
public class Result<T> {
    public final Exception exception;
    public final T value;
    private final ResultState state;

    public Result(T value) {
        this.exception = null;
        this.value = value;
        state = ResultState.SUCCESS;
    }

    public Result(Exception e) {
        this.exception = e;
        this.value = null;
        state = ResultState.FAULTED;
    }

    public static <T> Result<T> of(T value) {
        return new Result<>(value);
    }

    public static <T> Result<T> of(Exception e) {
        return new Result<>(e);
    }

    public boolean isFaulted() {
        return state == ResultState.FAULTED;
    }

    public boolean isSuccess() {
        return state == ResultState.SUCCESS;
    }
    public <R> R match(Function<T, R> successFn, Function<Exception, R> fail) {
        try {
            if (state == ResultState.FAULTED)
                return fail.apply(this.exception);
            else
                return successFn.apply(this.value);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public <R> R match(Supplier<R> successFn, Supplier<R> fail) {
        try {
            if (state == ResultState.FAULTED)
                return fail.get();
            else
                return successFn.get();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public <R> R match(Function<T, R> successFn, Supplier<R> fail) {
        try {
            if (state == ResultState.FAULTED)
                return fail.get();
            else
                return successFn.apply(this.value);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public <R> R match(Supplier<R> successFn, Function<Exception, R>  fail) {
        try {
            if (state == ResultState.FAULTED)
                return fail.apply(this.exception);
            else
                return successFn.get();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Usage

Repository

  @Override
    public Result<AdditionalRequirementResponse> create(AdditionalRequirementRequest tagRequest) {
        if (additionalRequirementData.values().stream().anyMatch(x -> x.name().equals(tagRequest.name())))
            return Result.of(new DuplicateNameException(ApplicationMessage.AdditionalRequirementMessage.ADDITIONAL_REQUIREMENT_NAME_DUPLICATE));

        var tag = additionalRequirementRequestMapper.apply(Optional.empty(),tagRequest);
        this.save(this.additionalRequirementData, tag);
        return Result.of(additionalRequirementResponseMapper.apply(tag));
    }
Enter fullscreen mode Exit fullscreen mode

Controller

    public HttpResponse<?> post(AdditionalRequirementRequest perkRequest) {
        var result = this.iEclipseStoreRepository.create(perkRequest);
        return result.match(success-> HttpResponse.ok(result.value), ()->HttpResponse.badRequest(result.exception.getMessage()));
    }
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

Using sealed interfaces + records for API responses provides:

  • Strong domain modeling
  • Explicit success/failure handling
  • Cleaner controllers
  • Compile-time exhaustiveness checking

This pattern scales well and works consistently across Micronaut, Spring Boot, and Quarkus, making it a solid choice for modern Java REST APIs.

Top comments (0)