Spring Boot — Power of Value Objects
Tags: java, programming, tutorial, design
Published: 2022-12-04 20:23:12

In this post, I’m telling you:

1. What are value objects and why are they so crucial?
2. How you can apply those patterns in your Spring Boot controllers to make code safer and easier to maintain.

I took code examples from [this repository](https://github.com/SimonHarmonicMinor/spring-boot-value-objects-example). You can clone it to see the entire project in action.

## Domain

The domain is rather simple. We have just a single entity. Anyway, you'll see that even one class may produce ambiguity and data corruption.

> I’m using Hibernate as a persistence framework. Therefore, the domain entity `User` is also a Hibernate entity. But the ideas I’m proposing to you remain the same even if you don’t use the Hibernate at all.

At first, let’s start with the database schema. There is one table. So, it won’t be difficult.

Supposing we want to store the user’s phone number. Here is the SQL table definition:

```sql
CREATE TABLE users
(
    id           UUID   PRIMARY KEY,
    phone_number VARCHAR(200) NOT NULL UNIQUE
);
```

The types are straight-forward. Now let's define the corresponding Hibernate entity. Look at the code example below.

```java
@Entity
@Table(name = "users")
@Getter
public class User {
    @Id
    private UUID id;

    @Column(name = "phone_number")
    private String phoneNumber;
}
```

Looks good, doesn’t it? Phone number is `String` type. The fields map to the database columns directly. What can go wrong? As you’ll see soon, lots of things.

## Phone number integrity issues

Is every possible `String` value a valid phone number? Of course, not. And that’s the problem. A user can put `0`, a negative value, or even `some-unknown-value-string` as a phone number.

Imagine that during the registration process, some user set their phone number to `-78005553535`. Obviously, there is a typo and there should be the `+` sign instead of the `-` one. Anyway, the user hasn’t noticed the mistake and applied the settings. Later, another user wants to find the previous folk and send an invitation to the group. He or she knows only the phone number. And suddenly the `+78005553535` search returns no result. Though the query input is absolutely correct. Now imagine that your application serves thousands of people. Even if 1% percent make a mistake in their phone number, the fixing values in the database will be tedious.

How you can overcome the issue? The answer is [value object](https://martinfowler.com/bliki/ValueObject.html). The idea is simple:

1. Value object has to be immutable.
2. Value object should be comparable (i.e. implements `equals/hashCode`).
3. Value object guarantees that it always holds the correct value.

Look at the the first attempt of `PhoneNumber` declaration below.

```java
@Value
public class PhoneNumber {
    String value;

    public PhoneNumber(String value) {
        this.value = value;
    }
}
```

> The [`@Value` Lombok annotation](https://projectlombok.org/features/Value) generates `equals`, `hashCode`, `toString` methods, getters and defines all fields as `private final`.

The `PhoneNumber` class solves the first and the second requirement that we’ve defined. However, you can still construct the class with invalid phone number (e.g. `0`, `-123`, `abc`). Meaning we should proceed with the validation process inside the constructor. Look at the fixed code snippet below.

```java
@Value
public class PhoneNumber {
    private static final PhoneNumberUtil PHONE_NUMBER_UTIL = PhoneNumberUtil.getInstance();

    String value;

    public PhoneNumber(String value) {
        this.value = value;
        validatePhoneNumber(value);
    }

    private static void validatePhoneNumber(String value) {
        try {
            if (Long.parseLong(value) <= 0) {
                throw new PhoneNumberParsingException("The phone number must be positive: " + value);
            }
            PHONE_NUMBER_UTIL.parse(String.valueOf(value), "RU");
        } catch (NumberParseException | NumberFormatException e) {
            throw new PhoneNumberParsingException("The phone number isn't valid: " + value, e);
        }
    }
}
```

> I'm using [Google Libphonenumber](https://github.com/google/libphonenumber) library to validate the input.

Seems like we solved the problem. We always validate the value during the object construction. However, a slight detail remains untouched. And it’s called _data normalization_.

If user `A` sets their phone number to `88005553535`, the user `B` won’t find him or her typing `+78005553535` value in the search bar. Though people in Russia treat these phone numbers as equal.

> It's a valid scenario for local calls only. Anyway, some users can make assumptions based on that.

As a matter of fact, we should always transform valid input values that are equal from the business perspective to the same output result to eliminate possible ambiguities. Look at the final `PhoneNumber` class declaration below.

```java
@Value
public class PhoneNumber {
    private static final PhoneNumberUtil PHONE_NUMBER_UTIL = PhoneNumberUtil.getInstance();

    String value;

    public PhoneNumber(String value) {
        this.value = validateAndNormalizePhoneNumber(value);
    }

    private static String validateAndNormalizePhoneNumber(String value) {
        try {
            if (Long.parseLong(value) <= 0) {
                throw new PhoneNumberParsingException("The phone number cannot be negative: " + value);
            }
            final var phoneNumber = PHONE_NUMBER_UTIL.parse(value, "RU");
            final String formattedPhoneNumber = PHONE_NUMBER_UTIL.format(phoneNumber, E164);
            // E164 format returns phone number with + character
            return formattedPhoneNumber.substring(1);
        } catch (NumberParseException | NumberFormatException e) {
            throw new PhoneNumberParsingException("The phone number isn't valid: " + value, e);
        }
    }
}
```

Let's also write some unit tests to verify the behavior.

```java
class PhoneNumberTest {
    @ParameterizedTest
    @CsvSource({
        "78005553535,78005553535",
        "88005553535,78005553535",
    })
    void shouldParsePhoneNumbersSuccessfully(String input, String expectedOutput) {
        final var phoneNumber = assertDoesNotThrow(
            () -> new PhoneNumber(input)
        );
        assertEquals(expectedOutput, phoneNumber.getValue());
    }

    @ParameterizedTest
    @ValueSource(strings = {
        "0", "-1", "-56"
    })
    void shouldThrowExceptionIfPhoneNumberIsNotValid(String input) {
        assertThrows(
            PhoneNumberParsingException.class,
            () -> new PhoneNumber(input)
        );
    }
}
```

And here is the execution result.

![Tests execution result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z1fkrb6wkfu6h5bfrjxb.png)

The last step is putting the value object to the `User` Hibernate entity. In this situation, the [AttributeConverter](https://www.baeldung.com/jpa-attribute-converters) comes in handy. Look at the code block below.

```java
@Converter
public class PhoneNumberConverter implements AttributeConverter<PhoneNumber, String> {
    @Override
    public String convertToDatabaseColumn(PhoneNumber attribute) {
        return attribute.getValue();
    }

    @Override
    public PhoneNumber convertToEntityAttribute(String dbData) {
        return new PhoneNumber(dbData);
    }
}

@Entity
@Table(name = "users")
@Getter
public class User {
    @Id
    private UUID id;

    @Column(name = "phone_number")
    @Convert(converter = PhoneNumberConverter.class)
    @NotNull
    private PhoneNumber phoneNumber;
}
``` 

What are the benefits of value objects in comparison to raw types using? Here they are:

1. If you receive the `PhoneNumber` instance, then you definitely know it is valid and you don’t need to repeat validations.
2. Fail-fast pattern. If the phone number is invalid, you quickly get an exception.
3. The code is more secure. If you don’t use raw types as business values at all, you guarantee that all input values have passed the defined checks.
4. If you’re a Hibernate user, then [JPQL](https://thorben-janssen.com/jpql/) queries will return the `PhoneNumber` value object but not just context-less `String` attribute.
5. You encapsulate all the checks in one class. If you have to adjust them according to the new business requirements, you should only do it in one place.

## Typed entities' IDs

You will probably have more than a single entity in your project. And there is a high chance that most of them will share the same type of the ID (in this case, the `UUID` type).

What's the problem with that? Supposing you have a service that assigns the `User` to some `UserGroup`. And it accepts two IDs as input parameters. Look at the code example below.

```java
public void assignUserToGroup(UUID userId, UUID userGroupId) { ... }
```

I bet you’ve seen dozens of similar snippets. Anyway, let’s assume that somebody has written this line of code.

```java
assignUserToGroup(userGroup.getId(), user.getId());
```

Can you spot a bug in here? We accidentally swapped the IDs. If you made such a mistake, you’d be lucky if you get [foreign key](https://www.w3schools.com/sql/sql_foreignkey.asp) violation on statement execution. But if you don’t have foreign keys on the table or the assignment has proceeded successfully and the business operation result is incorrect, then you’re in a big trouble.

I'm putting a slight change to the `assignUserToGroup` method declaration. Look at the fixed option below.

```java
public void assignUserToGroup(User.ID userId, UserGroup.ID userGroupId) { ... }
```

Now the swapping IDs bug is impossible. Because it would lead to compile time error. What’s better is that you can easily implement the approach to the Hibernate entity.

```java
@Entity
@Table(name = "users")
@Getter
public class User {
    @EmbeddedId
    private User.ID id;

    @Column(name = "phone_number")
    @Convert(converter = PhoneNumberConverter.class)
    @NotNull
    private PhoneNumber phoneNumber;

    @Data
    @Setter(PRIVATE)
    @Embeddable
    @AllArgsConstructor
    @NoArgsConstructor(access = PROTECTED)
    public static class ID implements Serializable {
        @Column(updatable = false)
        @NotNull
        private UUID id;
    }
}
```

All the `findBy` Spring Data queries, all the custom JPQL statements work with `User.ID` but not the raw `UUID` type. Besides, it also helps with method overloading. If you’re a raw IDs user and you need the same method accepting distinct entities’ IDs, then you have to name them differently. But with typed IDs, it’s not the case. Look at the example below.

```java
public class RoleOperations {

    // doesn't compile
    public boolean hasAnyRole(UUID userId, Role... role) {...}
    
    public boolean hasAnyRole(UUID userGroupId, Role... role) {...}
}

public class RoleOperations {

    // compiles successfully
    public boolean hasAnyRole(User.ID userId, Role... role) {...}
    
    public boolean hasAnyRole(UserGroup.ID userGroupId, Role... role) {...}
}
```

> Unfortunately, you cannot smoothly wrap the sequence-based IDs with value objects. There are solutions, but they are rather cumbersome. I left [the proposal to the Hibernate types project](https://github.com/vladmihalcea/hibernate-types/issues/522) of adding support to this feature. You’ve already seen the benefits of user-defined types. So, you can rate up my issue to make it more popular. However, you can still generate number IDs on the client side. For example, the [TSID library](https://github.com/f4b6a3/tsid-creator) does the job.

## REST endpoints parameters

I’ve pointed out that using value objects in all application levels reduces code complexity and makes it safer. However, when it comes to REST endpoints, things aren’t that simple. Assuming we need two operations:

1. Creating the new `User` with the given `PhoneNumber`.
2. Searching the existing `User` by the provided `PhoneNumber`.

Look at the possible implementation below.

```java
@RestController
class UserController {

    @PostMapping("/api/user")
    void createUser(@RequestParam String phoneNumber) {
        ...
    }

    @GetMapping("/api/user")
    UserResponse getUserByPhoneNumber(@RequestParam String phoneNumber) {
        ...
    }

    record UserResponse(UUID id, String phoneNumber) {
    }
}
```

As you can see, we came back again to the raw types usage (i.e. `UUID` as the id and `String` as the phone number). If we simply replace the `@RequestParam` type to `PhoneNumber`, we’ll get an exception in runtime. The `UserResponse` serialization won’t probably produce any errors, but the client will receive the data in the unexpected format. Because [Jackson](https://www.baeldung.com/jackson) (the default serialization library in Spring Boot) don’t know how to handle custom types.

Thankfully, there is a solution. Firstly, let's define a `SerdeProvider` interface.

```java
public interface SerdeProvider<T> {
    JsonDeserializer<T> getJsonDeserializer();
    JsonSerializer<T> getJsonSerializer();
    Formatter<T> getTypedFieldFormatter();
    Class<T> getType();
}
```

Then we need two implementations registered as Spring beans. The `PhoneNumberSerdeProvider` and the `UserIdSerdeProvider`. Look at the declaration below.

```java
@Component
class PhoneNumberSerdeProvider implements SerdeProvider<PhoneNumber> {
    @Override
    public JsonDeserializer<PhoneNumber> getJsonDeserializer() {
        return new JsonDeserializer<>() {
            @Override
            public PhoneNumber deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
                final var value = p.getValueAsString();
                if (value == null) {
                    return null;
                }
                return new PhoneNumber(value);
            }
        };
    }

    @Override
    public JsonSerializer<PhoneNumber> getJsonSerializer() {
        return new JsonSerializer<>() {
            @Override
            public void serialize(PhoneNumber value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
                if (value == null) {
                    gen.writeNull();
                } else {
                    gen.writeString(value.getValue());
                }
            }
        };
    }

    @Override
    public Formatter<PhoneNumber> getTypedFieldFormatter() {
        return new Formatter<>() {
            @Override
            public PhoneNumber parse(String text, Locale locale) {
                return new PhoneNumber(text);
            }

            @Override
            public String print(PhoneNumber object, Locale locale) {
                return object.getValue();
            }
        };
    }

    @Override
    public Class<PhoneNumber> getType() {
        return PhoneNumber.class;
    }
}
```

> The `UserIdSerdeProvider` implementation is similar. You can find the [source code in the repository](https://github.com/SimonHarmonicMinor/spring-boot-value-objects-example/blob/master/src/main/java/com/example/config/UserIdSerdeProvider.java).

And now we just need to register those custom providers to the `ObjectMapper` instance. Look at the Spring `@Configuration` below.

```java
@Slf4j
@Configuration
@RequiredArgsConstructor
@SuppressWarnings({"unchecked", "rawtypes"})
class WebMvcConfig implements WebMvcConfigurer {
    private final List<SerdeProvider<?>> serdeProviders;

    @Override
    public void addFormatters(FormatterRegistry registry) {
        for (SerdeProvider<?> provider : serdeProviders) {
            log.info("Add custom formatter for field type '{}'", provider.getType());
            registry.addFormatterForFieldType(provider.getType(), provider.getTypedFieldFormatter());
        }
    }

    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper()
            .registerModule(new Jdk8Module())
            .registerModule(new JavaTimeModule())
            .registerModule(customSerDeModule())
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    }

    public com.fasterxml.jackson.databind.Module customSerDeModule() {
        final var module = new SimpleModule("Custom SerDe module");
        for (SerdeProvider provider : serdeProviders) {
            log.info("Add custom serde for type '{}'", provider.getType());
            module.addSerializer(provider.getType(), provider.getJsonSerializer());
            module.addDeserializer(provider.getType(), provider.getJsonDeserializer());
        }
        return module;
    }
}
```

Afterwards, we can refactor the initial REST controller. Look at the final version below.

```java
@RestController
class UserController {

    @PostMapping("/api/user")
    void createUser(@RequestParam PhoneNumber phoneNumber) {
        ...
    }

    @GetMapping("/api/user")
    UserResponse getUserByPhoneNumber(@RequestParam PhoneNumber phoneNumber) {
        ...
    }

    record UserResponse(User.ID id, PhoneNumber phoneNumber) {
    }
}
```

As a result, we don’t have to deal with raw types in our application anymore. The framework converts the input values to the corresponding value objects and vice versa. Therefore, Spring automatically validates the value objects during their creation. So, if the input is invalid, you get the exception as early as possible without touching business logic at all. Amazing!

## Conclusion

Value objects are extremely powerful. On the one hand, it makes codes easier to maintain and helps to read it almost like a plain English text. Also, it also makes it safer because you always validate the input on a value object instantiation. I can also recommend you to read [this brilliant book](https://www.amazon.com/Secure-Design-Daniel-Deogun/dp/1617294357) about value objects and domain primitives. I got an inspiration for the article by reading this piece.

If you have any questions or suggestions, leave your comments down below. Thanks for reading!

## Resources

1. [The repository with code examples](https://github.com/SimonHarmonicMinor/spring-boot-value-objects-example)
1. [Value object definition](https://martinfowler.com/bliki/ValueObject.html)
1. [`@Value` Lombok annotation](https://projectlombok.org/features/Value)
1. [Google Libphonenumber library](https://github.com/google/libphonenumber)
1. [JPA AttributeConverter](https://www.baeldung.com/jpa-attribute-converters)
1. [JPQL](https://thorben-janssen.com/jpql/)
1. [Database foreign key definition](https://www.w3schools.com/sql/sql_foreignkey.asp) 
1. [My proposal to implement typed sequence IDs in Hibernate types project](https://github.com/vladmihalcea/hibernate-types/issues/522)
1. [Jackson library](https://www.baeldung.com/jackson)
1. [Secure by Design book](https://www.amazon.com/Secure-Design-Daniel-Deogun/dp/1617294357)
1. [TSID library](https://github.com/f4b6a3/tsid-creator)