3

In his book "Implementing Domain-Driven Design" Vaughn Vernon suggests to reference aggregate roots only by ID. I see the following advantages of this approach:

  • It is clear where the consistency boundary, so the aggregate root, ends
  • Accidential modifications of related objects can not happen
  • You're not running so easily in eager/lazy loading problems, because you're forced to implement a separate way for querying related objects at once.
  • You can provide just an ID instead of a complete aggregate root, what is esspecially useful when writing tests
  • Testing becomes even easiert if no foreign keys are used on the database

However, espcially the last point seems to be problematic from a validation point of view. It might be easy then to pass IDs of non-existent objects, so that an invalid object would be constructed. Another problem might be, if a referenced object is removed without notice by the referencing aggregate root.

To solve the first problem, the existence of referenced objects could be checked:

class BookService(
    private val authorRepository: AuthorRepository
) {
    fun create(
        title: String,
        isbn: String,
        authorId: AuthorId
    ): Book {
        require(authorRepository.exists(authorId))
        return Book(title, isbn, authorId)
    }
}

But then you could also use foreign keys at the database level, because you're also forced to provide related objects (like Author in my example). So, at least the advantage of simplified tests would no longer be existent.

How does the book notice when the author is removed from the system? That could be done with an event + listener. But then again, this is something databases solved decades ago with foreign keys.

Without references by ID only, the book would just accept an Author instance and obviously the author would have to exist then. But if tit has to exist, one could also use the foreign keys!

From my point of view this boils down to two reasonable options:

  • Not checking the existence of reference objects at all
  • Using foreign keys between between aggregate roots in the database

Questions: Would you consider it acceptable to not check the references at all? Would you use foreign keys? Do see a better option?

Related

2
  • If you can ensure that an Author instance can only exist if the author exists, then you can also ensure that an AuthorId can only exist if the author exists, or am I missing something? It is of course up to the internals of the repository or aggregate root or whatever to maintain the consistency of the underlying data. Sometimes, the best solution is to relax consistency requirements a bit.
    – amon
    Jan 27, 2022 at 11:33
  • "You can provide just an ID instead of a complete aggregate root, what is esspecially useful when writing tests" - eeeh, I don't know about that... In your tests, are you pulling these objects from the database? Jan 27, 2022 at 21:22

2 Answers 2

4

Foreign keys are to databases what security belts are to cars: they can prevent damage caused by ill-behaving applications or unanticipated circumstances. But you should better prevent those accidents in the first place.

First of all, foreign key validations by the database come too late. Normally you should make sure well before that the key would be valid. It really makes no sense to let a user enter all her flight details for a flight that doesn’t exist. But even with all the care you may take in the application, in a complex system, another user may delete that reference that was just checked. Here the seat-belt may come into play.

Now is it reasonnable to have no foreign key check at all? It depends, but there are plenty of situations where this makes sense. For example, because the key is managed in another system (or another microservice?), or because the key refers to non-essential data and could be corrected later (e.g. social security numbers, ISBN number, etc….). In the later case some extra logic may be needed (e.g. state that tells that some references are in error).

Conclusions: there is no silver bullet. You need to think case by case about the pros and cons.

1

TL;DR: Yes, for most cases there is a better option. That is to concentrate on behavior / functionality instead of modeling the database.

If you think about foreign keys and potential lazy loading problems it is clear that you are thinking about the database. You probably model database rows as objects, that is why you are concerned with technical details.

Your problem comes from elevating a technical detail to the same level as "real" behavior should be, so you are constantly trying to match the two. You'll have a much easier time, if you hide persistence details below business-relevant functionality. This means whenever you need to access the database you are in the context of a known business function. Which means you know exactly how to optimize, lazy-load, paginate, call a stored procedure, or do whatever it takes to get the specific job done.

What you're likely doing is you're trying to do "persistence" in a business-agnostic way. That is, trying to load/store without context. Which means you have to do the right thing for all circumstances. This likely ends up being sub-optimal for most cases. Even on the face of it that doesn't make sense.

I don't think Vaughn Vernon's style leads to a good design at all. Specifically referencing objects in the same codebase with ids makes no sense to me at all.

1
  • 1
    I'm following a domain-first approach - not database first. My thought was more: If I'm enforcing certain relationships in the domain, I could use foreign keys in the database as an additional safety net, because in some cases it is easy to make an implementation mistake that could easily be detected using foreign keys.
    – deamon
    Jan 31, 2022 at 20:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.