j’ai acheté un PC...
The Entity Pattern and Metadata Management

The Entity Pattern and Metadata Management

[Edit: ] ⏱ 9 mn

Some problems are so common that every developer with a bit of experience has faced them at some point. One of them is the distinction between persisted objects and those used in your services.

What follows is a train of thought, nothing more. It’s not meant to be “the best way,” or even better or worse than another. Use it, or don’t, as a base for your own ideas. And feel free to share yours with me.

DAO, DTO, POJO

Once upon a time, it was common practice (if not outright fashionable) to bundle logic into business objects. The DAO not only held business data but also the methods and business logic, and sometimes even persistence logic. I’m talking about a time that the under-20 crowd wouldn’t know…

Nowadays, modern applications tend toward a service-oriented architecture. Classes are instantiated as Singletons, contain no data, and hold all the domain logic, while POJOs carry only the data. This style is especially encouraged with Java 14’s Record feature, which introduces immutability and makes for Value Objects.

The Identifier Problem

This brings us to a recurring issue basically present in every application that deals with CRUD operations and storage.

How do you handle the ID of these objects ?

When your application creates an entity and needs to persist it, there’s a moment when the entity has no ID until it is saved and one is generated. The code manipulates an object that lacks an identifier for a while.

You could assign an ID at creation time, which sometimes is the best option. But quite often, ID generation should remain an implementation detail that the business logic doesn’t need to be aware of. IDs might be generated by a database sequence (not ideal, but occasionally useful) or a generic, domain-agnostic method. In those cases, the domain that creates the entity doesn’t assign an ID.

So you end up with entities that may or may not have an ID.

A common approach is to have a nullable id field on the entity. But this forces you into null checks everywhere—which gets tedious.

And you should never use Optional to handle nullable IDs. In general, avoid using Optional for object properties. It’s poorly suited for that, serializes badly in both JSON and binary, and makes your objects unnecessarily heavy. Optional should only be used for method results to avoid returning null.

This also clashes with immutability, since you’d have to mutate the object to assign it an ID.

Another pattern is to use DTO that include the ID. So you end up with a pair of twin objects: one for persistence (with ID) and one for service (without ID). This approach doesn’t work well in domain-driven designs. It leads to a lot of mapping code and sometimes leaks persistence objects into the domain layer.

Entity Composition

One approach I like is entity composition. It’s elegant and works very well in immutable architectures. It’s a bit verbose and can be complex to serialize/deserialize, but it’s functional.

We define a generic Entity class composed of an ID and the business object.

No, this is not a Pair. Yes, it’s similar, but using a pair would harm readability.

Pairs are bad .

Here’s what the code could look like:

public final class Entity<T> {
    public final String id;
    public final T self;

    public Entity(String id, T self) {
        this.id = id;
        this.self = self;
    }
}

// Or the record version

public record Entity<T>(String id, T self) {}

The benefit is that you don’t need to mutate or map objects anymore. Switching between the two forms is trivial.

However, serialization isn’t as pretty. Ideally, you’d want JSON like this:

{
    "id": "66",
    "name": "Kylo Ren Light Saber",
    "color": "RED",
    "blade": 3
}

But with composition, you get this:

{
    "id": "66",
    "self": {
        "name": "Kylo Ren Light Saber",
        "color": "RED",
        "blade": 3
    }
}

It works, but it’s less clean and harder for front-end code to consume.

To improve serialization, use Jackson (or another library):

@JsonPropertyOrder({"id", "self"})
public abstract class EntityJacksonMixin {
    @JsonProperty("_id")
    public String id;

    @JsonUnwrapped
    public Object self;
}

This Mixin flattens the entity for serialization, making it easier for the front-end while preserving backend structure.

For deserialization, if need, it’s trickier, code available on GitHub .

Metadata Management

Note the underscore (_) prefix for the id field. It hints at the semantics: the ID is just metadata, not domain data. Once you acknowledge that the ID is not a business concern, it becomes just another piece of metadata—like creation date or the user who created the object. Something the business layer may need to read or pass along at some point, but which should never be modified by the user. The underscore acts as a visual cue in serialized entities to signal fields that are immutable or not intended to be changed.

Building on this idea, we can add other metadata such as the object’s creation date or the identifier of the user who created it. This can be especially useful when you need to track or audit entities.

public record Entity<T>(
    String id, 
    Instant createdAt,
    String createdBy,
    T self
) {}

Just update the mixin accordingly and the resulting serialized object might look like this:

{
    "_id": "66",
    "_createdAt": "2024-03-23T17:12:42Z",
    "_createdBy": "US42",
    "name": "Kylo Ren Light Saber",
    "color": "RED",
    "blade": 3
}

This approach will work well for a while but presents several issues. First, all Entity objects have the same metadata. Specified or null, but they all have the same. Second, each time we want to add data, all objects are impacted. This will be very difficult to maintain. However, if your application is simple and you have limited metadata needs, this is probably the best way to do it.

To solve the problems of the last version, the classic solution is to make metadata no longer properties but a Map of key/value pairs. New problem: Map is one of the heaviest structures in the language. Putting them everywhere to manage metadata that might potentially be empty is clearly counterproductive.

However, EnumMap is relatively efficient and could be a good alternative. Moreover, it forces the definition of an Enum type for each object type and thus describes the metadata that each object can contain.

The new version would look something like this:

public record Entity<E extends Enum<E>, T>(
    String id, 
    EnumMap<E, Object> metas,
    T self
) {}

Metadata is managed optimally, and the Entity class is generic enough to be used for all objects in an application. But it could be improved. Creating such an entity has become very verbose and frankly not aesthetic. We end up with 2 generic elements, which does not really help with usage. And in the case of entities that do not have metadata, we pay the cost of the Map, or its nullity, which will force us to test at each use.

The Sealed Entity

To simplify the use of entities, we can add an interface that will hide the complexity of the objects behind it.

public sealed interface Entity<T> permits BasicEntity, ExtendedEntity {
    String id();

    <M> Optional<M> meta(Enum<?> property, Class<M> type);

    default Optional<String> meta(Enum<?> property) {
        return meta(property, String.class);
    }

    T self();

    static <T> BasicEntityBuilder<T> identify(@NotNull T entity) {
        return new BasicEntityBuilder<>(Objects.requireNonNull(entity, "Self entity must not be null !"));
    }
}

This interface is sealed to only be implemented by the basic form of the entity or the extended form. The basic form contains only the identifier, while the extended form contains the identifier and an EnumMap of metadata. This way, for basic entities that do not have metadata, we do not pay the additional cost of the EnumMap, either in structure or in usage.

Finally, two builders will hide the complexity of creating these objects. The static method identify serves as the starting point for creation. The choice of builder is made based on whether there are metadata to add.

public final class BasicEntityBuilder<T> {
    private final T self;

    public BasicEntityBuilder(T self) {
        this.self = self;
    }

    public <E extends Enum<E>> ExtendedEntityBuilder<T, E> meta(E property, Object value) {
        if (value == null) {
            return new ExtendedEntityBuilder<>(self, property.getDeclaringClass());
        }
        if (property instanceof TypedMeta typed && !typed.type().isAssignableFrom(value.getClass())) {
            throw new IllegalArgumentException("Value type "
                    + value.getClass() + "incompatible with "
                    + typed.type() + " from " + property.getClass());
        }
        return new ExtendedEntityBuilder<>(self, property.getDeclaringClass())
                .meta(property, value);
    }

    public Entity<T> withId(@NotNull String id) {
        Objects.requireNonNull(id, "ID is mandatory for Entity !");
        return new BasicEntity<>(id, this.self);
    }
}

The entity is therefore created as follows:

Entity.identify("May the force").withId("4TH");

Entity.identify("May the force")
    .meta(MetaEnum.createdBy, "Yoda")
    .meta(MetaEnum.createdAt, "2024-03-23T17:12:42Z")
    .withId("4TH");

The final version is available as a micro-library on GitHub and published on MavenCentral .

The library also contains functionality to serialize and deserialize entities using Jackson. Mixin annotations are no longer sufficient, and a feature/bug in Jackson prevents the use of polymorphism and wrapped entities at the same time .

Usage avec GraphQL

We have seen how we can serialize our Entities via Jackson and make the result a bit sexier by flattening the object. But for GraphQL , it is a bit more complex. Indeed, it is not a mapper that is used during type resolution for GraphQL but a DataFetcher.

The library provides an EntityDataFetcher that, accompanied by an EntityTypeResolver, will allow GraphQL to directly fetch objects wrapped in an Entity. For example, if you use Spring for GraphQL , you will only need to create a configuration class like this:

@Configuration
public class JediModuleConfiguration {
    @Bean
    public RuntimeWiringConfigurer jediRuntimeWiringConfigurer(ObjectMapper mapper) {
        DataFetcher<Object> dataFetcher = new EntityDataFetcher(mapper);
        TypeResolver typeResolver = new EntityTypeResolver();

        return wiringBuilder -> wiringBuilder
                .type(Jedi.class.getSimpleName(), builder -> builder.defaultDataFetcher(dataFetcher).typeResolver(typeResolver));
    }
}

And your GraphQL controllers will be able to return Entity<Jedi> objects directly.

@Controller
public class JedisController {
    @QueryMapping
    Flux<Entity<Jedi>> findJedis(@Arguments FindJediRequest request) {
        return jediService.find(request);
    }
}

Conclusion

Conclusion

This strategy is, of course, not perfect; for example, we lose the type of the enum when using the interface. But it is functional and really practical to use. Moreover, this scheme adapts to many uses; I have used it in almost all my projects, both personal and professional. This is what I use in particular for Baywatch .

Feel free to test it, with or without the library, and give me your feedback; I am curious.


The Entity Pattern and Metadata Management was published on and updated the last time on