hibernatevalidationormdomain-driven-designvalue-objects

Hibernate, Value Object and old data backward compatibility problem


I find Value Object pattern really satisfying. Lots of books like Secure by Design, Implementing Domain-Driven Design, and Domain Modeling Made Functional advocate to apply Value Object to make domain types clearer and more type safe.

For example, suppose I have Order entity that contains name. I could mark the name field as String but Value Object pattern suggests me to create a separate class OrderName. And the constructor may contain validations for the name of an Order. Look at the code example below.

@Entity
class Order {
    ...
    @Embedded
    private OrderName name;
}

@Data
@Setter(PRIVATE)
@Embeddedable
class OrderName extends SelfValidated {
    // custom bean validation annotation
    @OrderNameValid
    private String value;

    public OrderName(String value) {
        // name should be no longer than 50 characters
        this.value = value;
        // call bean validator manually
        validateSelf();
    }

    protected OrderName() {
        // called by Hibernate
        // Hibernate automatically scans bean validation annotaions
        // and invokes validator each time value object is persisted/loaded
    }
}

At first glance, everything looks good. I cannot create OrderName instance, if the passed input violates stated business rules (in this case, max length is 50 characters). But what if requirements change? Suppose that business decides that an order name max length should be 30 characters. It means that I cannot work with old orders. Because I have to read the row from the database and create the Order entity, if I need to alter it somehow.

I've been thinking about this problem for a long time. I can think about 4 solutions but I don't consider neither of them as completely valid. So, I wonder do you have any thoughts about it? I'd be glad to hear your opinions. Anyway, here are my ways to overcome the issue.

Update old data in the database to match new business rules

This one is straightforward. If data is not valid, change it so it becomes valid. Developers tend to add such updates as Flyway or Liquibase migrations. This can work under particular circumstances. Anyway, the solution is far from perfect. Here is why:

  1. Some data can be stored as a complex structure (e.g. jsonb object). Meaning that update itself shall be rather untrivial and cumbersome.
  2. Those update migrations are diffucult to test. If you run a database instance with Testcontainers and apply migrations immediteately after it, then they will run on the empty set of data. So, the brand new update will have no effect. You have re-run those scripts separately as a single test case.
  3. Sometimes you just cannot update old data. In some cases, this is not an option. You have to deal somehow with data is present right now.

Apply Value Object as input-only parameter

I came up with this solution by myself and even gave a talk at the conference about it. The idea is simple. Look at the code example below.

@Entity
class Order {
    ...
    private String name;

    // pass value object to update Order state
    public void changeName(OrderName name) {
        // unwrap it to store raw value
        this.name = name.getValue();
    }
}

As you can see, Hibernate entity stores name as a raw String type. But if we want to change it, then public method accepts OrderName Value Object which is unwrapped afterward. When you read data from the database, Hibernate creates an entity instance with no-args constructor and sets values to fields via Java Reflection API. It gives us several opportunities:

  1. Value Objects are still being part of the public API at the domain level
  2. If you accept Value Object as a paramemeter, you're absolutely sure that it corresponds with the current business rules
  3. If requirements' changes aren't backward compatible, it doesn't block you from reading old data.

However, this approach still have some drawbacks:

  1. The code becomes harder and less obvious. It might be unclear why do we store values as raw types but accept Value Object as an input
  2. If you have another Order method that needs its name for further operations, you cannot construct value object from it. Because if you do, you may get an exception due to invalidity of old data. So, even inside domain level you still work with old data
  3. If you want to get an order's name, you also cannot it wrap it with value object (read the previous point).

It seems like here Value Object's usage is not self-explanatory. And that's why comes the final option.

Value Objects should be validated only inside public constructor

In this case, Value Objects are validated only if you create them in your code directly. Otherwise, if Hibernate instantiates Value Object with Java Reflection API, then the value is not validated and just being set as-is. Look at the code example below:

@Data
@Setter(PRIVATE)
@Embeddedable
class OrderName {
    private String value;

    public OrderName(String value) {
        // name should be no longer than 50 characters
        this.value = validateValue(value);
    }

    protected OrderName() {
        // called by Hibernate
        // No validations happens here
    }
}

The problem of backward compatibility is no longer an issue. However, the idea of Value Object is broken. The whole point of Value Object is that it cannot be instantiated with invalid data. Though now it's not the case. Because Hibernate can create OrderName with invalid value by calling protected constructor.

If you apply such solution, then your code becomes not so resilient. Look at the code example below:

@Entity
class Order {
    ...
    private String name;

    public void changeName(OrderName name) {
        // is it a valid object?
        this.name = name;
    }
}

What if passed OrderName has been created by Hibernate but not by client. In such scenario, I cannot guarantee that OrderName corresponds with the current validation rules. So, the code transforms to something like this:

@Entity
class Order {
    ...
    private String name;

    public void changeName(OrderName name) {
        if (name.isValid()) {
            this.name = name;
        } else {
            throw new OrderNameNotValidException(...);
        }
    }
}

If I allow a Value Object to be either valid or invalid, I get no benefits of using it. Besides, I even make code harder and less straightforward.

Don't use value objects at all

So, if Value Object pattern brings us so many obstacles, maybe we just need to throw it away? Besides, some IT experts like Allen Holub claim that Value Object is an anti-pattern as the entire idea. Maybe it is, not sure about that.


I'll be glad to read your opinions about this problem. Especially if you're Hibernate user just like me. I'd really appreciate any valuable advice.


Solution

  • I would draw attention to the concept of the correctness of ValueObjects. In my opinion, a ValueObject created from old data stored in a database is considered correct. If database migration is not possible due to business requirements, it is logical to assume that business requirements specify behavior for previously created objects through business rules, such as requiring the user to manually change the name before performing operations on the object; operations on objects created before the application of new business requirements follow a separate logic path from the one followed by objects created after the appearance of the business requirements. In this case, there are no contradictions in implementing the ValueObject, but unfortunately, it will not protect it from increasing complexity - which in this case is natural.