Can a repository access another by DDD?
Asked Answered
F

2

1

I am practicing DDD, and I have a very simple example, which looks like this currently:

Polling
    getEventBus() -> Bus
    getEventStorage() -> Storage
    getMemberRepository() -> MemberRepository
    getCategoryRepository() -> CategoryRepository
    getBrandRepository() -> BrandRepository
    getModelRepository() -> ModelRepository
    getVoteRepository() -> VoteRepository

MemberRepository
    MemberRepository(eventBus, eventStorage)
    registerMember(id, uri)
        -> MemberRegistered(id, uri, date)
        -> MemberRegistrationFailed //when id or uri is not unique
    isMemberWithIdRegistered(id)
    isMemberWithUriRegistered(uri)

CategoryRepository
    CategoryRepository(eventBus, eventStorage) {
    addCategory(id, name)
        -> CategoryAdded(id, name, date)
        -> CategoryAdditionFailed //when id or name is not unique
    isCategoryWithIdAdded(id)
    isCategoryWithNameAdded(name)
};

BrandRepository
    CategoryRepository(eventBus, eventStorage) {
    addBrand(id, name)
        -> BrandAdded(id, name, date)
        -> BrandAdditionFailed //when id or name is not unique
    isBrandWithIdAdded(id)
    isBrandWithNameAdded(name)
};

ModelRepository
    ModelRepository(eventBus, eventStorage)
    addModel(id, name, categoryId, brandId)
        -> ModelAdded(id, name, categoryId, brandId, date)
        -> ModelAdditionFailed //when id or name is not unique and when category or brand is not recognized
    isModelWithIdAdded(id)
    isModelWithNameAdded(name)

VoteRepository
    VoteRepository(eventBus, eventStorage)
    addVote(memberId, modelId, vote, uri)
        -> MemberVoted(memberId, modelId, vote, uri, date)
        -> VoteFailed //when the member already voted on the actual model and when memberId or modelId is not recognized

I'd like to develop here a polling system, so I think we could call this the polling domain. We have members, categories, brands, models and votes. Each member can vote on a model only once and each model have a brand and a category. For example inf3rno can vote on the Shoe: Mizuno - Wave Rider 19 with 10, because he really likes it.

My problem is with the

addModel(id, name, categoryId, brandId)
    -> ModelAdded(id, name, categoryId, brandId, date)
    -> ModelAdditionFailed //when id or name is not unique and when category or brand is not recognized

and the

addVote(memberId, modelId, vote, uri)
    -> MemberVoted(memberId, modelId, vote, uri, date)
    -> VoteFailed //when the member already voted on the actual model and when memberId or modelId is not recognized

parts. Let's stick with the ModelAddtion.

If I want to check whether the categoryId and brandId are valid, I have to call the CategoryRepository.isCategoryWithIdAdded(categoryId) and the BrandRepository.isBrandWithIdAdded(brandId) methods. Is it allowed to access these methods from the ModelRepository? Should I inject the container and use the getCategoryRepository() -> CategoryRepository and getBrandRepository() -> BrandRepository methods? How to solve this properly by DDD?

update:

How would you solve this validation in the domain if you'd really need the foreign key constraint and your db engine would not have this feature?

Folksy answered 2/7, 2016 at 14:36 Comment(0)
I
6

There are 2 hard problems in computer science: cache invalidation, naming things, off by one errors, and attributing quotes.... I'll come in again.

Repository, as used in the ubiquitous language of DDD itself, doesn't normally mean what you are trying to express here.

Eric Evans wrote (the Blue Book, chapter 6).

Another transition that exposes technical complexity that can swamp the domain design is the transition to and from storage. This transition is the responsibility of another domain design construct, the REPOSITORY

The idea is to hide all the inner workings from the client, so that client code will be the same whether the data is stored in an object database, stored in a relational database, or simply held in memory.

In other words, the interface of a repository defines a contract to be implemented by the persistence component.

MemberRepository
    MemberRepository(eventBus, eventStorage)
        registerMember(id, uri)
        -> MemberRegistered(id, uri, date)
        -> MemberRegistrationFailed //when id or uri is not unique

This, on the other hand, looks like a modification to your domain model. "registerUser" has the semantics of a command, MemberRegistered, MemberRegistrationFailed look like domain events, which strongly implies that this thing is an aggregate, which is to say an entity that protects specific invariants within the domain.

Naming one of your aggregates "Repository" is going to confuse everybody. The names of aggregates should really be taken from the ubiquitous language of the bounded context, not from the pattern language we use to describe the implementation.

If I want to check whether the categoryId and brandId are valid, I have to call the CategoryRepository.isCategoryWithIdAdded(categoryId) and the BrandRepository.isBrandWithIdAdded(brandId) methods. Is it allowed to access these methods from the ModelRepository?

Assuming, as above, that CategoryRepository, BrandRepository and ModelRepository are all aggregates, the answer is no, no, and no.

No: If your have modeled your domain correctly, then all of the state needed to ensure that a change is consistent with the business invariant should be included within the boundary of the aggregate that is changing. Consider, for example, what it would mean to be adding a model in this thread, while the brand that the model needs is being removed in that thread. These are separate transactions, which means that the model can't maintain the consistency invariant.

No: if the motivation for the check it to reduce the incidence of errors by sanitizing the inputs, that logic really belongs within the application component, not the domain model. It's the responsibility of the domain model to ensure that the parameters of the command induce a valid change to the state of the model; it's the responsibility of the application to ensure that the correct parameters are being passed. The sanity check belongs outside the domain model

That said

No: aggregates in the domain model shouldn't access each other directly; instead of passing in an aggregate, pass in a domain service that represents the query that the domain model needs to run.

Model.addModel(brandId, brandLookupService) {
    if (brandLookupService.isValid(brandId)) {
        // ...
    }
}

This extra bit of indirection removes any ambiguity about which aggregate is being changed within a given transaction. The BrandLookupService itself, under the covers, could well be loading a read only representation of a Brand from the BrandRepository.

Of course, it still doesn't address the concern that the brands could be changing even as the model is referencing the brand. In other words, there's a potential data race in this design because of where the transactions boundaries are drawn.

How would you solve this validation in the domain if you'd really need the foreign key constraint and your db engine would not have this feature?

Two options:

1) Redraw the aggregate boundaries.

If you need the foreign key constraint enforced by the domain model, then its not a "foreign" key; its a local key for an aggregate that contains both bits of state.

2) Change the requirements

Udi Dahan, I think in this talk, pointed out that sometimes the way that the business (currently) runs simply doesn't scale properly, and the business itself may need to change to get the results that they want.

I am not sure what the aggregates are here.

Let's try this a different way - how do we implement this?

For example inf3rno can vote on the Shoe: Mizuno - Wave Rider 19 with 10, because he really likes it.

In your design above, you used a VoteRepository to do this. We don't want to use "repository", because that noun isn't taken from the ubiquitous language. You called this the polling domain earlier, so let's try Poll as the entity. The Poll entity is going to be responsible for enforcing the "one man, one vote" invariant.

So it's going to look something like

class Poll {
    private PollId id;
    private Map<MemberId,Vote> recordedVotes;

    public void recordVote(MemberId memberId, Vote vote) {
        if (recordedVotes.containsKey(memberId)) {
            throw VoteFailed("This member already voted.  No backsies!");
        }
        recordedVotes.put(memberId, vote);
    }
}

And the code to record the vote is going to look something like

// Vote is just a value type, we can create one whenever we need to

Vote vote = Vote.create(10);

// entity ids are also value types that we can create whenever
// we want.  In a real program, we've probably done both of these
// lookups already; Poll and Member are entities, which implies that
// their identity is immutable - we don't need to worry that
// MemberId 3a7fdc5e-36d4-45e2-b21c-942a4f68e35d has been assigned
// to a different member.

PollId pollId = PollId.for("Mizuno - WaveRider 19")
MemberId memberId = MemberId.for("inf3rno");

Poll thePoll = pollRepository.get(pollId);
thePoll.recordVote(memberId, vote);
pollRepository.save(thePoll);
Inwrought answered 2/7, 2016 at 16:11 Comment(7)
I suspected that something is wrong, because I did not need aggregates by this design... If I understand well I need something like this code in an application service: modelRepository.save(aModelFactory.createModel(brandId, brandLookupService, ...)). I am not sure what the aggregates are here. What I need is adding categories, brands, models, registering members and let them vote on each model only once. According to your post the model should be an aggregate here, but I should be able to add e.g. brands, and adding brands does not touch any model. How should I add brands then?Folksy
A lot of great DDD practitionners such as Vaughn Vernon will pass aggregates as arguments to other aggregates. As long as the other AR doesn't only hold onto it's identity it often expresses the UL much better than passing identities around. poll.recordVote(member, ...) is more aligned with the UL than poll.recordVote(memberId, ...).Acuate
@Folksy I do not think that you understand what an aggregate is and understanding aggregates is key to DDD tactical patterns. Just read the three parts of Effective Aggregate Design.Acuate
@Acuate Thanks, but I want to understand aggregates by this exact example. I read Vernon's book (implementing ddd), I doubt that more reading about general stuff would help.Folksy
Repositories just persist aggregates and retrieve them from the persisted storage. Most of the logic is concentrated in aggregates itself and in application services (aka command handlers). Repositories are essentially persisted collections, nothing more. IDDD is a good book about the implementation, however the principles are better described in the blue book. If you do not want to spend time reading, there are couple of courses on Pluralsight and DDD Distilled (green) book by Vernon.Underwrite
Which layer the last code fragment is written in?Extine
In a layered design, you'd be most likely to see that fragment in the "application layer", I believe.Inwrought
P
1

From a puristic view, you shouldn't need to access 2 repositories. I say puristic because it might take a while to understand what missing bits of the domain would simplify this.

From the top of my head, I would question myself the following:

  • Do you need to ensure that those entities exist? (isCategoryWithIdAdded and isBrandWithIdAdded). Depending on your storage engine, can you enforce this (e.g. required foreign key). I think this would be my approach, as it's also faster from a performance point of view.
  • Can you ensure those entities exist somewhere else? Some DDD implementations assume that data is correct once an Application Service is called.
  • And last (this might be a bit questionable) can this feature link 2 things even if they don't exist? (what would be the 'damage').

And just a comment... having something in your domain called Model is so confusing, as it's part of the vocabulary of DDD. =D

Peripeteia answered 2/7, 2016 at 15:27 Comment(3)
Some of them would have an impact, e.g. if somebody tries to add a category, brand or product (model), which already exists, it would lead to duplication. Allowing votes with unrecognized member or product id would not have a huge impact on the system and it would boost voting performance. The same stands for adding products too. I doubt that letting the storage engine to enforce invariants is a good approach, because the code can break by db migration. Btw. we are currently talking about an event storage, so using foreign keys is not an option.Folksy
What is really interesting here, that I don't need domain objects because I use CQRS and I need only create and read yet. :-) I can move the validation to the repositories. I know this is not a best practice. :-)Folksy
Okay, let's put this into another perspective. How would you solve this in the domain if you'd really need the foreign key constraint and your db engine would not have this feature? :-)Folksy

© 2022 - 2024 — McMap. All rights reserved.