Automated conversion between immutable business objects and MessagePack messages

915 views Asked by At

In Java, I would like to use hierarchies of immutable POJOs to express my domain model.

e.g.

final ServiceId id = new ServiceId(ServiceType.Foo, "my-foo-service")
final ServiceConfig cfg = new ServiceConfig("localhost", 8080, "abc", JvmConfig.DEFAULT)
final ServiceInfo info = new ServiceInfo(id, cfg)

All of these POJOs have public final fields with no getters or setters. (If you are a fan of getters, please pretend that the fields are private with getters.)

I would also like to serialize these objects using the MessagePack library in order to pass them around over the network, store them to ZooKeeper nodes, etc.

The problem is that MessagePack only supports serialization of public, non-final fields, so I cannot serialize the business objects as-is. Also MessagePack does not support enum, so I have to convert enum values to int or String for serialization. (Yes it does, if you add an annotation to your enums. See my comment below.)

To deal with this I have a hand-written corresponding hierarchy of "message" objects, with conversions between each business object and its corresponding message object. Obviously this is not ideal because it causes a large amount of duplicated code, and human error could result in missing fields, etc.

Are there any better solutions to this problem?

  • Code generation at compile time?
  • Some way to generate the appropriate serializable classes at runtime?
  • Give up on MessagePack?
  • Give up on immutability and enums in my business objects?
  • Is there some kind of generic wrapper library that can wrap a mutable object (the message object) into an immutable one (the business object)?

MessagePack also supports serialization of Java Beans (using the @MessagePackBeans annotation), so if I can automatically convert an immutable object to/from a Java Bean, that may get me closer to a solution.

2

There are 2 answers

1
Software Engineer On BEST ANSWER

It sounds like you have merged, rather than separated, the read and write concerns of your application. You should probably consider CQRS at this point.

In my experience, immutable domain objects are almost always attached to an audit story (requirement), or it's lookup data (enums).

Your domain should probably be, mostly, mutable, but you still don't need getters and setters. Instead you should have verbs on your objects which result in a modified domain model, and which raise events when something interesting happens in the domain (interesting to the business -- business == someone paying for your time). It's probably the events that you're interested in passing over the wire, not the domain objects. Maybe it's even the commands (these are similar to events, but the source is an agent external to the bounded context in which your domain lives -- events are internal to the model's bounded context).

You can have a service to persist the events (and another one to persist commands), which is also your audit-log (fulfilling your audit stories).

You can have an event handler that pushes your events onto your bus. These events should contain either simple information or entity ID's. The services that respond to these events should perform their duties using the information provided, or they should query for the information they need using the given ID's.

You really shouldn't be exposing the internal state of your domain model. You're breaking encapsulation by doing that, and that's not really a desirable thing to do. If I were you I'd take a look at the Axon Framework. It's likely to get you further than MessagePack alone.

1
cambecc On

Coincidentally, I recently created a project that does pretty much exactly what you are describing. The use of immutable data models provides huge benefits, but many serialization technologies seem to approach immutability as an afterthought. I wanted something that would fix this.

My project, Grains, uses code generation to create an immutable implementation of a domain model. The implementation is generic enough that it can be adapted to different serialization frameworks. MessagePack, Jackson, Kryo, and standard Java serialization are supported so far.

Just write a set of interfaces that describe your domain model. For example:

public interface ServiceId {
    enum ServiceType {Foo, Bar}

    String getName();
    ServiceType getType();
}

public interface ServiceConfig {
    enum JvmConfig {DEFAULT, SPECIAL}

    String getHost();
    int getPort();
    String getUser();
    JvmConfig getType();
}

public interface ServiceInfo {
    ServiceId getId();
    ServiceConfig getConfig();
}

The Grains Maven plugin then generates immutable implementations of these interfaces at compile time. (The source it generates is designed to be read by humans.) You then create instances of your objects. This example shows two construction patterns:

ServiceIdGrain id = ServiceIdFactory.defaultValue()
    .withType(ServiceType.Foo)
    .withName("my-foo-service");

ServiceConfigBuilder cfg = ServiceConfigFactory.newBuilder()
    .setHost("localhost")
    .setPort(8080)
    .setUser("abc")
    .setType(JvmConfig.DEFAULT);

ServiceInfoGrain info = ServiceInfoFactory.defaultValue()
    .withId(id)
    .withConfig(cfg.build());

Not as simple as your public final fields, I know, but inheritance and composition are not possible without getters and setters. And, these objects are easily read and written with MessagePack:

MessagePack msgpack = MessagePackTools.newGrainsMessagePack();

byte[] data = msgpack.write(info);
ServiceInfoGrain unpacked = msgpack.read(data, ServiceInfoGrain.class);

If the Grains framework doesn't work for you, feel free to inspect its MessagePack templates. You can write a generic TemplateBuilder that uses reflection to set the final fields of your hand-written domain model. The trick is to create a custom TemplateRegistry that allows registration of your custom builder.