Domain specific types in a play framework project.

We can all agree that it’s useful to be able to express a number. Or a character. It could be argued that most code is built upon some set of primitives. And naturally simple types work well in a limited number of use cases: algorithms, tutorials, small/focused applications, etc.

The moment the domain outgrows the natural usage of a given primitive it becomes troublesome to keep in mind what the meaning behind a particular Int is. We all know this, it’s been the driving force behind OOP since day one.

Recently I’ve been working on an application based on play framework 2.4.x and slick 3.0. Unsurprisingly, as the app grew the need to track and control domain-specific data became more and more pressing.

Type aliases

It’s useful to be able to quickly start with something simple. Scala has type aliasing which provides a simplistic way of expressing intent.

Quick & easy but has some drawbacks:

The above code compiles fine but Mike might not be all that happy about it. The solution itself isn’t that bad: gives no type safety and serves only as a hint but it’s very cheap and can easily be employed as a ‘stub’ for a better solution. It’s important to stress that this approach doesn’t scale well: domain specific aliases x amount of developers x features implemented equals a misassignment bound to happen.

Unboxed Tagged Types

After reading this post I’ve decided to try if this approach could work with playand slick. The gist of this approach is that it’s possible to attach ‘meta information’ to a primitive type and the typer will enforce it.

Implementation (scalaz):

In practice it looks like this:

Mike can be at ease:

produces:

Having the basics covered I got to the interesting parts.

Helpful ‘Int’ extension:

Removes .asInstanceOf[A] invocations and converts primitives into domain types:

Slick mapper

Usage in table definition:

Play json formatting:

Play routes

This is where the trouble starts. To be able to use custom types in routes a path binding needs to be provided:

routes file:

And the error:

It turns out that:

is what play framework generates in Routes.scala.

The ramifications are quite annoying – instead of having type checking in routes file it’s necessary to handle boxing manually:

Controller:

In tests:

It’s worth keeping in mind that unboxes tagged types and play jsondon’t align perfectly.

The proposed solution covers some of the aspects that would make it a valid solution for handling domain specific types. It falls short in a very important spot: ease of testing. The test code base should be (in my opinion) easy to maintain and refactor and loosing type safety in this area is a deal breaker.

At this point I was out of ideas on how to push this solution further so I decided to give a completely different approach a try.

Case classes

Case classes come with a set of limitations, but when the problem calls for type-safety, readability and ease of integration with common libraries (in this case play and slick), they quickly become valuable tools.

Value class with slick’s mapping

Json serialization

Int implicit extension

Play path binding

This way routes can utilize the type system:

and in tests:

A lot of the code base in a DB centric application will call for values: names, identifiers, nicks, etc. Not all of these need to be processed and if not – the above approach makes building applications easier and less error prone.

Macros

The previous paragraph shows how to integrate case classes with the stack but it introduces boilerplate. It’s not a lot but it seems wasteful and might be error prone. Maybe we can use some macro magic to make it go away?

Here’s my take on the problem, heavily inspired by this article.

Annotation

annotation expansion

def macros

Providing def macros and utilizing them in the annotation macro made the json format visible for other already defined macros (e.g. Json.format). This way if another class uses one of the annotated case classes and defines an implicit Json.format the compiler won’t complain.

build.sbt macros section

One way to make the macro compilation order happy in a play application:

Usage

This solution uses macro paradise so it’s quite volatile (read experimentalsubject to change etc.) but it makes adding new simple domain specific types very straightforward: path bindingsjson serialization and slick integration just work ™ for all primitive types.

One thing that was omitted from the macro is primitive type extension.

Summary

It would be so useful to be able to reason about a primitive and have the compiler police it’s domain constraints. Disappointing but understandable – library authors have a lot of interop requirements to appease and a niche feature that seems to be a tad outside of how scala programming should look like isn’t a priority.

Case classes may not be the perfect solution but the approach is popular enough for library authors to provide out-of-the-box helpers that just work (e.g. slick’s MappedTo). And if not: it’s always possible (and quite easy) to figure out a macro or two that will meddle with the case class itself or its companion object and deal with boilerplate around the required implicits.

Links

Do you like this post? Want to stay updated? Follow us on Twitter or subscribe to our Feed.

See also

Authors

Dominik Zajkowski

Latest Blogposts

26.03.2023 / By  Daria Karasek

Scaling Applications: Best Practices and Strategies

As your application gains popularity and usage, ensuring it can handle the increased traffic and demand is essential. Scaling your application increases its capacity to handle more users and data. This guide offers tips and best practices for scaling your applications effectively. Understand Your Application’s Architecture. Before you can scale your application, it’s essential to […]

24.03.2023 / By  Daria Karasek

Maximizing Your Apache Kafka Investment with Consulting Services

If you’re using Apache Kafka for your data streaming needs, you may face challenges or want to optimize your implementation. Our consulting services can guide you in improving your Kafka setup, addressing issues, implementing best practices, and utilizing new features. What is Apache Kafka? Firstly, companies use Apache Kafka as an open-source distributed event streaming […]

20.03.2023 / By  Daria Karasek

7 Tips for a Successful Development Career in Fintech

Fintech is one of the world’s most exciting and fastest-growing industries. The fintech industry is worth approximately $180 billion (Deloitte). The fast-growing space is projected to reach $174 billion in 2023. And is predicted to reach $188 billion by 2024. It offers a unique set of career opportunities for future fintech developers.  If you’re thinking about beginning a […]

Need a successful project?

Estimate project