The Programming Bibliophile

Tiny Types: Avoid stringly-typed systems

I am a big fan of tiny types also known as micro types or value types. The idea is simple - all primitives and strings in your code are wrapped by a class, which means you’ll never pass any primitives around.

The problem which we are trying to solve is to avoid illegal values entering your system. For this, it is best to use strongly typed values, which allows you to both lean on the compiler and improve the developer experience by engaging with IDE tooling.

The parse, don’t validate mantra is all about parsing incoming data to a specific type, or failing in a controlled manner if the parsing fails. It’s about using trusted, safe and typed data structures inside your code and making sure all incoming data is handled at the very edges of your system. Don’t pass incoming data deep into your code, parse it right away and fail fast if needed!

Take the following sendEmail method for example. You can’t distinguish between the recipient’s email address, subject or body because they are all strings. The only way to know how to call the method correctly is by paying close attention to the argument names and their position.

public void sendEmail(
    String recipient, 
    String subject, 
    String body
) {
    // ...
}

Having to deal with non type-safe methods can easily lead to confusion and is a common source of bugs in my experience. For example, the sendEmail method expects the second argument to be a string containing the subject of the email. It would be an easy mistake to make to swap the subject with the body of the email. Additionally, the sendEmail method will need to check whether the provided arguments are valid. For example, can the body or the subject be an empty, blank or null string?

// compiles without errors
sendEmail(
    "alice@example.com", 
    "Hello Alice, I hope ...", // body - should be the last argument!
    "Reminder: Please RSVP" // subject - should be the second argument!
);

Depending on your setup you might not be able to find this bug quickly. We can achieve type-safety by applying the concept of tiny types to the sendEmail method.

public void sendEmail(
    Recipient recipient,
    Subject subject,
    Body body
) {
    // ...
}

Once applied, the compiler will fail to compile the code if we’re trying to invoke the sendEmail method with arguments in the wrong position. The instant feedback from the compiler is the beauty using tiny types.

// fails to compile: expected Recipient got Subject
sendEmail(
    Subject.of("Reminder: Please RSVP"),
    Recipient.of("alice@example.com"),
    Body.of("Hello Alice")
);

// compiles without errors
sendEmail(
    Recipient.of("alice@example.com"),
    Subject.of("Reminder: Please RSVP"),
    Body.of("Hello Alice")
);

Tiny types become even more powerful when applied at the edges of your system as previously mentioned (parse, don’t validate). They enforce that only valid values are allowed to enter your system through a REST interface, persistence layer, etc. This in turn will simplify your business logic because you can trust that the data you are given is valid.

A simple and ad-hoc approach to implementing tiny types is to use Java’s record classes to define your domain objects including validation at the point of creation!

import static java.util.Objects.requireNonNull;

public record Subject(String value) {
    public Subject {
        if (requireNonNull(value).isBlank()) 
            throw new IllegalArgumentException("must not be blank");
    }

    public static Subject of(String value) {
        return new Subject(value);
    }
}

Alternatively, you can use my toastshaman/tiny-types implementation. Another great library if you are using Kotlin is forkhandles/values4k.

If we were to expose the sendEmail function through a REST interface we might write a Spring Boot controller to accept a JSON payload.

{
  "recipient": "alice@example.com",
  "subject": "Reminder: Please RSVP",
  "body": "Hello Alice"
}

Which we want to map to a Java POJO using tiny types.

public record SendEmailRequest(
  Recipient recipient,
  Subject subject,
  Body body
) {}

Spring Boot uses jackson-databind to map JSON objects to Java POJOs. We have to tell Jackson how to deserialize plain JSON strings into our recipient, subject and body tiny types. Jackson allows you to define your own custom serialzers and deserializers by writing a SimpleModule.

@Component
public class TinyTypeModule extends SimpleModule {

    public TinyTypeModule() {
        text(Subject.class, Subject::new, Subject::value);
        text(Recipient.class, Recipient::new, Recipient::value);
        text(Body.class, Body::new, Body::value);
    }

    private  <T> void text(Class<T> type,
                           Function<String, T> creatorFn,
                           Function<T, String> showFn) {
        addDeserializer(type, new JsonDeserializer<>() {
            @Override
            public T deserialize(JsonParser p,
                                 DeserializationContext ctxt) throws IOException {
                return creatorFn.apply(p.getText());
            }
        });

        addSerializer(type, new JsonSerializer<T>() {
            @Override
            public void serialize(T value,
                                  JsonGenerator gen,
                                  SerializerProvider serializers) throws IOException {
                gen.writeString(showFn.apply(value));
            }
        });
    }
}

If you are using Spring Boot, the @Component annotation on your SimpleModule will ensure that the module will be registered with the global ObjectMapper instance of your Spring Boot application.

@RestController
public class EmailController {
    private final Emails emails;

    public EmailController(Emails emails) {
        this.emails = emails;
    }

    public record SendEmailRequest(
            Recipient recipient,
            Subject subject,
            Body body
    ) {}

    @PostMapping("/emails")
    public ResponseEntity<Void> sendEmail(
            @RequestBody SendEmailRequest request
    ) {
        try {
            // at this point we know the data is valid!
            emails.sendEmail(request.recipient, request.subject, request.body);
            return ResponseEntity.accepted().build();
        } catch (Exception e) {
            return ResponseEntity.internalServerError().build();
        }
    }
}

We’ve now removed all the primitives from our REST interface and avoided a stringly-typed system. We’ve validated the data at the edge of the system before we process the data further. We can lean on the compiler and it’s type checking to enforce rules and relationships between classes which will greatly improve the soundness and maintainability of the system.