3

During prototyping a simple ddd application from public transit domain I've faced a problem with one value object - Transit Pass:

enter image description here

Each Customer can buy a transit Pass thatallows a passenger of the service to take either a certain number of pre-purchased trips or unlimited trips within a fixed period of time. A detailed description of these passes is represented in instances of PassDefinitions. A Customer selects PassDefinition that is most appropriate for him and receives a new Pass.

In the real word, Customer Account has Passes so it's pretty straightforward to put Pass into Account aggregate as a value object that has a pointer to PassDefinition. This approach has 2 problems:

  • What if administrator will delete PassDefinition? Some Pass objects may point to deleted PassDefinition and inconsistency in context will arise.
  • There will be a method (hasEligiblePass) on Account and since Pass has no information about validity, vehicleAllowed etc external call to PassDefinition will be required.

Another option is to move Pass into PassDefinition aggregate. But:

  • What if Customer will delete Account (while it is unlikely since the account can be activated/deactivated only by design)?
  • It's very common operation in the system to list all available Passes for specific Account. Putting Pass into PassDefinition will force such trivial operation to retrieve PassDefinitions and filter many results.

So the global problem is that Pass lifecycle depends on Account and PassDefinition lifecycles. How to deal with such objects in DDD?

  • A value object doesn't have a lifecycle. Pass should be an entity here. The definition details should be copied over to the Pass entity as values. Changing the definition shouldn't affect passes that were already purchased. – plalx Dec 15 '16 at 15:14
  • If you are concerned about disk space usage, version the pass definitions and reference a specific version from the pass. – plalx Dec 15 '16 at 15:20
  • 1
    If it's entirely immutable then it's probably not necessary to copy the data, especially if multiple passes will be created from the same definitions, but you could if you do not care about storage space and want to keep your model simple. Another approach would be to treat the immutable internals of the PassDefinition as a VO from the model perspective, but an entity from the persistence perspective effectively allowing you to store the VO only once, but in the model Pass would hold a reference to the definition internals as a VO. – plalx Dec 15 '16 at 18:59
  • 1
    Yet another alternative could be to break the reference ARs by identity rule and reference a PassDefinition AR directly inside a Pass. Since PassDefinition is mostly immutable it shouldn't be a problem. The last option is to keep your model as it is, but resolve the dependencies at the application service level and pass the details to the pass. E.g. PassDefinition def = passDefinitionRepo.findById(defId); bool eligible = account.hasEligiblePass(def);. Within hasEligiblePass you could check if the id of the passed definition matches the pass's definition id. – plalx Dec 15 '16 at 19:03
  • 1
    If you go down that road you have to be certain that the PassDefinition AR will always be immutable. – plalx Dec 15 '16 at 19:05
5

PassDefinition looks suspiciously like a name invented by a programmer, rather than something taken from the ubiquitous language. You should probably sit down with your domain experts and hash out what's really happening in the business.

My guess - you are confusing two (or more) concepts: that of the contract established when a Customer purchases a Pass, and that of the offering in the sales catalog when the customer considering a purchase.

Taking the problem as you present it: it is fairly common to have state that references two different aggregates. That usually means another aggregate with its own life cycle that represents the relationship between the aggregates.

Of course, in such a design changes to the aggregate in the middle are isolated from the current state of each of the reference aggregates. All of the state required to validate a change to the model has to be in one place.

  • Indeed, PassDefinition is invented by me at the point of creating this question :) In the real system, this class has another, more domain-specific name, but I've named it PassDefinition to make it more straightforward for non-domain related devs here. – ovnia Dec 14 '16 at 14:59
  • 2
    As a general rule, with DDD you want to be cautious about abstracting/generalizing/simplifying the problem you are really facing -- the details tend to matter much more. On the other hand, your domain model code tends to be where your business advantage lives, so there's a delicate balance to be discovered on a case by case basis. – VoiceOfUnreason Dec 14 '16 at 16:10
1

If you put CustomerId on Pass and move it in with PassDefinition you can have

PassDomain.Pass
PassDomain.PassDefinition
PassDomain.PassRepository

PassRepository.GetValidPassFor(customerId,vehicleType,journeyDate)

Maybe split customerId into ownerId + ownerType to avoid the assumed link to Customer

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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