DEV Community

Andreas Jim
Andreas Jim

Posted on • Updated on

Safe Domain Objects with MapStruct and Immutables

Even in Java you are not doomed to deal with mutable objects and nullable fields. With the help of annotation processor libraries like MapStruct and Immutables you can add some safety and convenience to your codebase.

During the last couple of months I have been working with Java, using mutable POJOs as domain objects and MyBatis for database access. Queries are declared in XML files, with only some basic and rather cumbersome support for modularisation and code reuse. Why would I want to deal with another XML-based expression language, when I have a perfectly capable programming language with complete IDE support at my disposal?

Without the compiler protecting me from my own mistakes and the dreaded NullPointerException lurking behind every corner, I dearly missed the comfort and safety of the Scala ecosystem. So I started wondering how to get something akin to case classes and Quill in the cold and harsh winter of the Java world. Let's start with a brief wishlist.

All I want for Christmas is

  • A substitute for case classes — immutable objects, preferrably with support for cloning (with modified fields) and optics.
  • Support for mapping primitive database fields to enums, value classes and complex types.
  • Dedicated domain objects for creating and reading (more on this below).
  • Type-safe, modular queries.
  • Minimal boilerplate would be the icing on the gingerbread biscuit.

Libraries

After a little bit of research, I identified the following libraries as possible candidates for the persistence and domain layers:

  • Immutables — Annotation processor to generate simple, safe and consistent value objects
  • MapStruct — Another annotation processor; we will use it to map between domain objects and data transfer objects (DTOs). I picked this library out of several similar ones since it seems to be mature and performant and supports Immutables out of the box.
  • Jinq — Type-safe queries
  • Functional Java — Because we can't have Cats.

The example code for this article can be found on GitHub.

The Domain Model

Let's start with our domain model. We define the interfaces of our domain objects and let Immutables generate the implementations.

@Value.Immutable @Tuple
public interface Book {

    String getTitle();

}
Enter fullscreen mode Exit fullscreen mode
@Value.Immutable @Tuple
public interface Character {

    String getName();

    Option<Species> getSpecies();

}
Enter fullscreen mode Exit fullscreen mode

A few things worth mentioning:

  • The @Tuple annotation denotes that we would like to use a single static method to initialize our objects, as opposed to a builder. This way we can ensure at compile-time that all fields are initialized.
  • The type Species is an enum. JPA can map this to an ordinal value.
  • We are using the Option class from the Functional Java library, a safer and more convenient alternative to Java's Optional. We use a custom JPA attribute converter to map this field to a nullable field in the DTO.

Domain object identifiers

You may have noticed that our domain classes are lacking an identifier which will mapped to a primary key in the DTO. This is by design: Primary keys are often automatically generated by the database. When we create a new domain object, it doesn't have an identifier yet. We could model it as an Option, but this wouldn't accurately reflect reality — a domain object never has an ID before it is persisted, and it always has an ID when it has been retrieved from the database. Instead, we wrap our domain object in a separate object to augment it with the ID:

@Value.Immutable
public interface WithId<ID, A> {

    ID getId();

    A getData();

}
Enter fullscreen mode Exit fullscreen mode

We will use value objects for our IDs as an additional safety measure. Luckily, Immutables supports them out of the box by means of wrapper types:

@Value.Immutable @Wrapped
abstract class _BookId extends Wrapper<Integer> {}
Enter fullscreen mode Exit fullscreen mode
@Value.Immutable @Wrapped
abstract class _CharacterId extends Wrapper<Integer> {}
Enter fullscreen mode Exit fullscreen mode

Based on these definitions, the Immutables annotation processor will generate the concrete classes BookId and CharacterId.

Last but not least, we define a many-to-many relation between books and characters:

@Value.Immutable @Tuple
public interface BookToCharacter {

    BookId getBookId();

    CharacterId getCharacterId();

}
Enter fullscreen mode Exit fullscreen mode

Data transfer objects (DTOs)

Due to the limitations of JPA, in addition to our domain classes, we have to use dedicated mutable classes for the data transfer objects. This results in quite a bit of knowledge duplication and boilerplate. If you know a solution for this problem, I would be very happy to hear about it in the comments section. It would more or less render this article obsolete, which I wouldn't be particularly unhappy about.

Since the DTOs are significantly more verbose than the domain objects, I will only show the most relevant sections.

@Entity
public class BookDto implements WithIdDto<BookId, Book> {

    @Override
    public WithId<BookId, Book> toEntity() {  }

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    private String title;

    // Getters and setters
}
Enter fullscreen mode Exit fullscreen mode
@Entity
public class CharacterDto implements WithIdDto<CharacterId, Character> {

    @Override
    public WithId<CharacterId, Character> toEntity() {  }

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    private String name;

    @Enumerated(EnumType.ORDINAL)
    @Convert(converter = OptionConverter.class)
    private Option<Species> species;

    // Getters and setters

}
Enter fullscreen mode Exit fullscreen mode

Both of these classes implement the WithIdDto interface, which defines the toEntity() method. This method allows us to convert a DTO to the domain object, including the ID which is derived from the primary key.

And, last but not least, the DTO for our relation, which uses a composite primary key. We are using an @IdClass instead of an @EmbeddedId to simplify the mapping from/to the domain class.

@Entity
@IdClass(BookToCharacterDto.BookToCharacterId.class)
public class BookToCharacterDto {

    public static class BookToCharacterId implements Serializable {

        private final int bookId;
        private final int characterId;

        // Constructor and getters
    }

    @Id
    private int bookId;

    @Id
    private int characterId;

    // Getters and setters
}
Enter fullscreen mode Exit fullscreen mode

Mapping between domain objects and DTOs

The heavy lifting is done by MapStruct, we just have to declare our mappers. First, we define a common interface for all domain-object-to-DTO mappers:

public interface DtoMapper<E, D> {

    D toDto(final E entity);

    E fromDto(final D dto);

}
Enter fullscreen mode Exit fullscreen mode

Now we declare the individual mappers:

import static org.mapstruct.factory.Mappers.getMapper;

public final class Mappers {

    @Mapper(uses = BookIdMapper.class)
    public interface BookMapper
            extends DtoMapper<Book, BookDto> {
        BookMapper instance = getMapper(BookMapper.class);
    }

    @Mapper(uses = CharacterIdMapper.class)
    public interface CharacterMapper
            extends DtoMapper<Character, CharacterDto> {
        CharacterMapper instance = getMapper(CharacterMapper.class);
    }

    @Mapper(uses = { BookIdMapper.class, CharacterIdMapper.class })
    public interface BookToCharacterMapper {
        BookToCharacterMapper instance =
                getMapper(BookToCharacterMapper.class);
        BookToCharacterDto toDto(final BookToCharacter entity);
    }

}
Enter fullscreen mode Exit fullscreen mode

Note that BookToCharacterMapper doesn't extend DtoMapper; for some reason MapStruct refused to generate the mapper class.

I implemented a custom mapper for each wrapper type. There is probably a way to get rid of this boilerplate, but it didn't bother me enough yet to spend more time on this issue:

public class BookIdMapper {

    public BookId asBookId(final int id) {
        return BookId.of(id);
    }

    public int asInt(final BookId bookId) {
        return bookId.value();
    }

}
Enter fullscreen mode Exit fullscreen mode

Finally, let's take a quick look at the implementation of the toEntity method in our DTOs:

@Entity
public class BookDto implements WithIdDto<BookId, Book> {

    @Override
    public WithId<BookId, Book> toEntity() {
        return ImmutableWithId.of(
                BookId.of(id),
                Mappers.BookMapper.instance.fromDto(this)
        );
    }

    // …
}
Enter fullscreen mode Exit fullscreen mode

Querying data

Now that we have declared our domain objects and defined how they are persisted, we can start using them. I won't go into the details of building queries with Jinq — this is covered extensively in the documentation — and rather just show some example code.

public class BookService extends AbstractPersistenceService {

    

    public Stream<P2<
                WithId<BookId, Book>,
                WithId<CharacterId, Character>
            >> getBooksWithCharacters() {
        return Stream
                .iterableStream(books(entityManager)
                .leftOuterJoin(
                        (b, source) -> source.stream(BookToCharacterDto.class),
                        (b, r) -> b.getId() == r.getBookId()
                )
                .leftOuterJoin(
                        (p, source) -> source.stream(CharacterDto.class),
                        (p, c) -> p.getTwo().getCharacterId() == c.getId()
                )
                .toList())
                .map(TupleOps::p2)
                .map(p -> p
                        .map1(TupleOps::p2)
                        .map1(P2::_1)
                        .map1(WithIdDto::toEntity)
                        .map2(WithIdDto::toEntity));
    }

}
Enter fullscreen mode Exit fullscreen mode

At this level, we still have to deal with the DTO classes, but only to build queries, so there is no danger of introducing mutable state.

The joins result in nested Jinq tuples. At the end we transform these tuples into FunctionalJava tuples, and map the contained DTOs to their respective entities. In a real-world project, I would make this code generic and reuse it across all services.

Using the service

Now we can take a look at our test method, which persists and reads domain objects using the service:

    @Test
    public void testPersistence() {
        transactional(() -> {

            final WithId<BookId, Book> book =
                bookService.createBook(ImmutableBook.of("Night Watch"));

            final Effect1<Character> addCharacter = (ch) -> {
                final WithId<CharacterId, Character> chRead =
                    bookService.createCharacter(ch);
                bookService.addCharacterToBook(
                    book.getId(), chRead.getId());
            };

            final Character vimes = ImmutableCharacter
                .of("Samuel Vimes", some(HUMAN));
            final Character nobby = ImmutableCharacter
                .copyOf(vimes)
                .withName("Nobby Nobbs")
                .withSpecies(none());

            addCharacter.f(vimes);
            addCharacter.f(nobby);

            final Stream<P2<
                WithId<BookId, Book>,
                WithId<CharacterId, Character>
            >> booksWithCharacters =
                    bookService.getBooksWithCharacters();

            assertTrue(booksWithCharacters.isNotEmpty());

            booksWithCharacters.foreach(tuple((bk, ch) -> {
                System.out.printf("%s (%s)%n",
                    ch.getData().getName(),
                    ch.getData().getSpecies());
                return Unit.unit();
            }));

        });
    }
Enter fullscreen mode Exit fullscreen mode

Summary

We have managed to address some critical pain points, in particular we can map between immutable domain objects and DTOs with a reasonable amout of boilerplate and uild type-safe queries using Java code.

Evidently, there is quite a bit of room for improvement:

  • Our implementation supports both the Builder style (for MapStruct) and the Tuple style (for our code). Using the Builder style can lead to runtime errors since it doesn't ensure at compile time that all fields are initialised. Please leave a comment if you know how to get MapStruct to use the Tuple style for mapping.
  • The error messages emitted by the MapStruct and Immutables annotation processors leave a lot to be desired. I'm not particularly fond of meta programming in general; this experience confirms my preconception. Maybe it is possible to get more information by setting compiler arguments, but my attempts were unsuccessful.
  • Jinq queries are not completely type-safe, there can still be runtime errors ("Could not analyze lambda code").
  • We are not treating effects as first-class citizens. The Functional Java library provides an IO type for this purpose, but this is a topic for a future article.

Discussion (1)

Collapse
ambitionconsulting profile image
John-Paul Cunliffe • Edited on

Great post - and funny how I ended up with the same stack independently.

You should probably mind/note the new type-safe staged builders from java immutables.

This will keep the types safe, make your (nested) instance creation better readable, and allows you (by formatting new builder stages on one line each) to easily refactor parameter replacing - by moving the line up or down.

Basically refactoring is the only problem (partly) unsolved with this stack - but with the staged building at least I am guaranteed to get accurate compile-time errors. In general I love it though.