Two layer repositories in Spring

Circular Forms - Moon No. 3, 1913 - Robert Delaunay

Persistence is one of the most difficult and confusing problems in object oriented programming, which, it might seem, will never be solved. Databases are about individual rows of data, objects are about individual instances of behaviour. It is not obvious how they can be mapped. And so, it is quite common that developers, whenever faced with persistence, revert to procedural programming, tearing their objects apart into data and procedures, persist the data and give it to the procedures to work on. The data “objects” have only fields (with constructor and/or auto-generated getters and setters) which are mapped somehow to table columns, and the procedures are usually put into stateless singleton “services”. This is the infamous Anemic Domain Model approach. However, with the comeback of object thinking in recent years, more and more people have revolted against this paradigm, and a search for different approaches, those preserving encapsulation and respecting anthropomorphised objects, have begun. Most popular data source patterns have been cast under suspicion, and one of the leading authors of this resurgent movement has even proposed abandoning ORMs altogether and making domain objects speak SQL.

I don’t think ORM is really that terrible. For sure, they way they are usually being used is terrible, and I will assume throughout this post that the reader strives towards object thinking and is in search of reconciliation of real objects and databases. If you are using Spring and are wondering how this can be achieved without having to write your own SQL, this post is for you.

Repository

The Repository pattern has been created precisely for this purpose. The term comes from Domain Driven Design and means a collection of domain objects, where “domain objects” are real, encapsulated, self-sufficient objects that represent real world (business) entities. Some people have been calling data structures persisting ORMs “repositories”, but these ORMs are as much repositories as data structures are domain entities, i.e. not at all. Spring Repositories have been created with this in mind1 - they are supposed to be used with real objects which expose real behaviour and not just act as data containers. Using repositories with anemic domain model is simply a misuse. It is really easy to keep data and behaviour together and use Spring Repositories as they were intended to be used, but they do have limitations, and in certain cases we need a second layer on top of them. But let’s begin with a simple example.

Anemia

Let’s take a look at a typical anemic domain model. We have Account and AccountService which look something like this:

@Entity
public class Account {
    @Id @Column private String iban;
    @Column private int ownerId;

    // Getters, setters...
}

@Service
public final class AccountService {
    public boolean isAccountOwnedBy(Account account, int customerId) {
        return account.getOwnerId() == customerId;
    }
}

Let’s say, the database table looks like this:

IBAN OWNER_ID
LT601010012345678901 000001
DK9520000123456789 000002

And then somewhere in the data access layer, the table row is mapped to the data “object”2:

Account account = new Account();
account.setIban(row.field(0).asString());
account.setOwnerId(row.field(1).asInt());

Let’s see how this could be implemented with Spring Data repositories. Just declare an interface3:

@Repository
public interface AccountRepository extends CrudRepository<Account, String> {
}

And that’s it. Spring will generate the implementation, and we can use it via standard methods inherited from CrudRepository:

public void openNewAccount(String iban, int customerId) {
    repository.save(new Account(iban, customerId));
}

public String getBalance(String iban, int requestorId) {
    Account account = repository.findById("LT601010012345678901").orElseThrow(...);
    if (accountService.isAccountOwnedBy(account, requestorId)) {
        // Calculate balance...
    } else {
        throw new IllegalArgumentException(
            "Account is not owned by " + requestorId
        );
    }
}

Simple behaviour

Like I already said, this is a misuse, but it is easy to fix. We use AccountService to determine if the account is owned by the requestor, before we proceed to calculate balance. To fix it, we can simply put methods with behaviour into the entity object - there is no need for procedures existing separately in a service. Then Account becomes a real cohesive object:

@Entity
public final class Account {
    @Id @Column private String iban;
    @Column private int ownerId;
    
    // Constructors...

    public boolean isOwnedBy(int customerId) {
        return this.ownerId == customerId;
    }
}

This way we can not only get rid of getters and setters but also of “service” classes - now instead of plucking owner’s ID from Account and giving it to “service” to decide if the requestor owns the account, we can leave the ID encapsulated and ask Account nicely:

if (account.isOwnedBy(requestorId)) {
    // Calculate balance...
}

The problem is not all behaviour is this easy to add. Hibernate (or any other JPA implementation we use with Spring Data) will instantiate the objects for us, and we will not be able to inject the needed collaborators via constructor (don’t try doing that via setters or passing them to business methods - you will regret it). This means the behaviour of our objects is limited to the fields which are fetched from persistence layer. It seems that our objects can either have data from database or collaborators which provide interesting behaviour, but not both. If we want to calculate account balance by fetching transactions for this account and adding them up, the Account object cannot do it. Since it is instantiated by Hibernate, we can’t inject Transactions instance into it. We have to do something like this, outside of the Account object, in some controller:

if (account.isOwnedBy(requestorId)) {
    return transactions.forAccount(account)
        .map(transaction -> transaction.amountFor(account))
        .reduce(Amount::plus)
        .orElse(new Amount(BigDecimal.ZERO, "EUR"));
}

Enabling collaboration

This is no good. We want to have the balance() method in the Account entity itself:

public interface Account {
    Amount balance();
    boolean isOwnedBy(int customer);
}

But in order to do that, we need to inject Transactions into the Account implementation so it looks like this:

public final class PersistedAccount implements Account {
    private final String iban;
    private final Transactions transactions;
    ...

    public PersistedAccount(String iban, Transactions transactions, ...) {
        this.iban = iban;
        this.transactions = transactions;
        ...
    }

    @Override
    public Amount balance() {
        return transactions.forAccount(this)
            .map(transaction -> transaction.amountFor(this))
            .reduce(Amount::plus)
            .orElse(new Amount(BigDecimal.ZERO, "EUR"));
    }
    
    // Other methods...
}

And then we should be able to get it from the repository and ask it to calculate balance without actually having to know how it will be done:

Account account = accounts.byIban(id).orElseThrow(...);
if (account.isOwnedBy(customer)) {
    return account.balance();
}

This accounts object here is a repository (remember - repository is a collection of business entities), but not from Spring - we will write it ourselves. It will encapsulate the Spring Repository and use it to fetch data from database, because it is excellent at that. We will instantiate our Account entity in this second layer.

public final class PersistedAccounts implements Accounts {
    private final AccountEntries entries;
    private final Transactions transactions;

    // Constructor...

    @Override
    public Optional<Account> byIban(String iban) {
        return entries.findById(iban).map(
            entry -> new PersistedAccount(
                entry.iban, entry.ownerId, entries, transactions
            )
        );
    }
}

See, it passes both the data from database (IBAN and owner’s ID) and the Transactions to the entity constructor - this is exactly what we needed. It also passes the Spring Repository (AccountEntries) so the PersistedAccount does not lose contact with database.

And the Spring Repository (the one encapsulated inside the real one) now simply maps from database rows to data structures4.

@Repository
public interface AccountEntries
    extends CrudRepository<AccountEntries.AccountEntry, String> {
    
    @Entity
    public class AccountEntry {
        @Id @Column public String iban;
        @Column public int ownerId;
        public AccountEntry(String iban, int ownerId) {
            this.iban = iban;
            this.ownerId = ownerId;
        }
    }
}

Notice it has its JPA structure (I won’t call it “JPA entity”, even though I am forced to annotate it like that) declared directly in the data repository interface, and it’s a simple data structure. AccountEntry’s fields are public - it doesn’t even pretend to be a real object, it might as well be a record set. It should be used only by the real object which owns it to communicate with AccountEntries, it should never leak in any domain model interface.

Now we have a two-layer repository composition, enabling us to fetch real, encapsulated and self-sufficient objects from database.

Insert

We can fetch accounts from database now, but how do we persist them? We can’t have a Accounts::save(Account) method because Account is encapsulated and the repository cannot access its fields. We could just give it IBAN and customer and ask it to insert them as account, but that would introduce naked data into the domain model5. My suggestion is to let the Account persist itself by adding an open() method:

public interface Account {
    Amount balance();
    boolean isOwnedBy(int customer);
    void open();
}

While it is perfectly OK for Account to persist itself, it might seem strange to have a method for it in the interface, because interface is supposed to define the domain model, and persistence is a technical detail - technical details must not leak into the model. There are several options for dealing with this. As far as accounts are concerned, having a method open() is fine, because business people are indeed talking about opening accounts and closing them. Therefore, open() belongs in the domain. Similarly, transactions can be booked, customers can be registered and so forth. Still, there might be objects which have to be persisted, yet there is no corresponding business activity, and the object ends up with save() or create() method. This is still better than breaking encapsulation or having naked data exposed in the domain model. If you are not satisfied, there is an alternative - introducing a bit of even sourcing and having something like new AccountOpenedEvent(iban, customer).execute(). This way the data is limited to the constructor and not exposed in any interface.

To implement open(), just create new AccountEntry and save it:

public final class PersistedAccount implements Account {
    private final String iban;
    private final int ownerId;
    private final AccountEntries entries;
    ...

    // Constructor, other methods...

    @Override
    public void open() {
        entries.save(
            new AccountEntries.AccountEntry(iban, ownerId)
        );
    }
}

Testing

Testing this code is extremely easy, and we don’t need any mocking libraries. Just create a fake implementation of Spring Repository:

public final class FakeAccountEntries implements AccountEntries {
    private final Set<AccountEntry> entries;

    public FakeAccountEntries() {
        this.entries = new HashSet<>();
    }

    @Override
    public Optional<AccountEntry> findById(String id) {
        return entries.stream()
            .filter(entry -> entry.iban.equals(id))
            .findAny();
    }

    @Override
    public <S extends AccountEntry> S save(S entry) {
        entries.remove(entry); // must override equals and hashCode
        entries.add(entry);
        return entry;
    }
    
    // ...
}

And inject it into your objects under test:

AccountEntries entries = new FakeAccountEntries();
Account account = new PersistedAccount(
    "LT601010012345678901",
    000001,
    entries,
    new FakeTransactions()
);
account.open();
assertTrue(entries.existsById("LT601010012345678901"));

They don’t speak SQL though?

One advantage SQL speaking objects have over two-layer repositories is that repositories have to fetch the data from database and feed it to its entities via constructor, while SQL speaking objects only get their IDs and can later fetch the data themselves. It is not difficult to tweak the two-layer repository design to enable our entities to do the same. Declare private methods for each property:

private int ownerId() {
    return entries.findById(iban).orElseThrow(...).ownerId;
}

and implement the appropriate interface methods:

@Override
public boolean isOwnedBy(int customer) {
    return ownerId() == customer;
}

Conclusion

We have divided the repository into two layers vertically: the Spring Repository which fetches rows from database, and the domain repository which encapsulates the Spring one and instantiates domain objects (entities) by giving them data and any collaborators they require. It can also be seen as collaborations between objects at various points, giving rise to a higher level of abstraction:

  • PersistedAccounts (lower level) collaborates with AccountEntries (lower level) to retrieve persisted accounts, thus implementing Accounts (higher level).
  • PersistedAccount (lower level) collaborates with AccountEntries (lower level) to open itself, thus implementing a part of Account (higher level).
  • PersistedAccount (lower level) collaborates with Transactions (higher level) to calculate its balance, thus implementing another part of Account (higher level).

This way, by implementing Two Layer Repositories we can have:

  • Real domain objects which are cohesive, encapsulated and self-sufficient (and which can communicate with the database as well as any SQL speaking object could).
  • No SQL, naked data or abstraction leakage in domain model.
  • No need to handle connection pooling, caching, DB transactions, etc. - Spring Data can do it automatically.
  • Extremely easy testing.

No approach is perfect though, and there are a few disadvantages:

  • There still are data “objects” left. No approach can fully eliminate them since we are dealing with database after all. As long as they are properly hidden inside real objects, they are no worse than record sets which would otherwise be used.
  • Spring is a heavy framework. This is a real drawback, but if you have already decided to use Spring and still want to do real object oriented programming, Two Layer Repositories is exactly what you need.
  1. From official Spring Repositories documentation: “Implementing a data access layer of an application has been cumbersome for quite a while. Too much boilerplate code had to be written. Domain classes were anemic and not designed in a real object oriented or domain driven manner. Using both of these technologies makes developers life a lot easier regarding rich domain model’s persistence.” [emphasis added] 

  2. It’s actually quite more complex, but it doesn’t matter for present purposes. 

  3. This post is not intended to be a Spring Data tutorial, I assume the reader is somewhat familiar with it. If you are not, you can read this. The purpose of this post is to explain how a second layer can be added on top of Spring Data repository so it can work with fully self-sufficient objects. 

  4. Earlier in this post I claimed it’s a misuse to use Spring Data Repositories to persist anemic “objects” and not real entities with behaviour. You may be wondering whether what we are doing now is not this exact misuse. It is not, because the intention is still to persist real objects with rich behaviour, even more rich than Spring Repository would have been able to handle otherwise. 

  5. The customerId parameter in isOwnedBy method is actually naked data - it’s int. It should be some Customer object, but let’s leave it int since the focus of this post is elsewhere. 

Written on April 7, 2019