Separating Persistence and Domain Models

Ürgo Ringo
3 min readSep 8, 2023

--

Building a good library for object-relational mapping is a complex task. On the one hand, you want to make it easy to define the mapping (mapping effort) but on the other hand, you don’t want to limit the kind of object graphs that can be persisted (complex model support).

Spring Data JDBC project has found a good balance between these two when using a relatively primitive domain model. It’s very easy to use and it does a pretty good job of reducing boilerplate in persistence mapping. However, when the domain model gets more complex it is less convenient. One issue is support for multilevel nested graphs (for example: a Subscription that contains Orders that each contain different OrderLines). This is where JOOQ really shines.

Another problem, I recently found is not supporting Kotlin value classes.

Inline value classes is a feature in Kotlin that allows the use of Value Objects with no runtime overhead (at runtime, these types get replaced by their underlying primitive). Our codebase was extensively using them to avoid primitively typed values so I had no desire to eliminate them just because of the persistence library.

Brute force solution

When faced with this problem my first reaction was to dig into the underlying library (Spring Data JDBC in this case) and find some magical combination of features/tricks to make it work.

I tried the following things among others:

  • “dear ChatGPT help me” — suggested using some nice imaginary features that do not exist
  • using custom factory methods with the @PersistenceCreator
  • using a constructor with primitives and mapping internally to value class types.

None of them was completely satisfying. Either the resulting domain model looked very convoluted or the idea simply wasn’t working with the version of Spring Data JDBC I was able to use in our project.

Separate domain and persistence model implementations

Then I realised that maybe instead of trying to fit the domain model and Spring Data JDBC into the same room I should just give up and create an internal persistence model that fits Spring Data JDBC. Then manually map that into the domain model.

Of course, this means having some boilerplate code. But in the days of AI coding assistants writing this kind of mapping is very low effort. Compared to the low-level mapping between ResultSet and domain objects when using plain Spring JdbcTemplate this code maps between two sets of custom types and hence is significantly cheaper to update/maintain.

Thanks to separating the persistence and the domain model I could completely ignore the persistence tool limitations when writing business logic. All this without having to do any of the low-level mapping manually — win-win.

Although this is an example with Kotlin and Spring Data JDBC, it’s really a more generic approach.

Sometimes it’s better to accept the differences between domain and persistence models and not force them into a single implementation.

For example, although JPA is pretty powerful in some cases it still requires altering the domain model just because a specific combination of relationships and types is not supported. There too using separate classes for JPA and domain can be a nice compromise.

Code example

A sample repository from our project looks like this:

class SubscriptionRepository(private val subscriptionTable: SubscriptionTable) {

fun findBy(customerId: CustomerId): Subscription? {
return subscriptionTable.findByCustomerId(customerId.toLong())?.toDomain()
}

fun save(subscription: Subscription) {
subscriptionTable.save(subscription.toData())
}
}

Here SubscriptionTable is the “repository” provided by Spring Data JDBC. I did not want to call it a Repository to avoid confusion. In this approach, it is an internal implementation detail.

Extension methods containing the mapping between persistence and domain model:

fun SubscriptionData.toDomain() = Subscription(
id = id,
customerId = CustomerId(customerId),
address = address,
...
orders = orders.map { it.toDomain() }.toList()
)

fun Subscription.toData() = SubscriptionData(
id = id,
customerId = customerId.toLong(),
address = address,
...
orders = orders.map { it.toData() }.toSet()
)

SubscriptionData is the persistence model type used to satisfy Spring Data JDBC and Subscription is the counterpart in the domain model.

Since SubscriptionData maps 1–1 to the database table then Spring Data JDBC takes care of all the mapping and generates SQL for it.

--

--

Ürgo Ringo

Have been creating software for 20 years. Cofounded a software consultancy, worked as an IC and team lead at Wise. Currently working at Inbank.