2

I have to design a part of a system in responsible for creating tickets for attractions and then sell them. Basically there are three different parts:

  1. Product: to create the tickets for sale for each attraction.
  2. Prices: to assign prices according to some criterias.
  3. Qutoas: to manage the available quotas.

I have to take care of point 1. Once the ticket is created, other people will be in charge of maintaining its selling prices and quotas.

I am interested in the part of creating and offering a ticket option. The user will be able to see what options are available for a 5 year old child and an adult (or other combination). The system will display a list of all the ticks that meet the search criteria.

Once an option is selected, another process will check if there is a quota and what price it has.

My problem is centered on step 1. Create the tickets.

From the point of view of creation, I think that there is the object Attraction. An attraction can be "Theme Park X" or a theater show or a museum. The attraction has a configuration that determines which TicketOptions can be created. For example, entry times, person categories by age, languages in case of a guided tour.

So an attraction might have a "General Admission Ticket" that is valid for both children and adults. But there may be a "Park + Night Show" ticket that will be created for adults only.

The Attraction is who has the configuration of how a ticket can be created. Therefore, the attraction would be the root. And it would have a list of TicketOption.

Apart from the creation it is also in charge of invalidating the tickets options created. If the attraction is classified as "adults only" in the future, it'll have to know which ticket exists in order to invalidate those for minors.

The problem is that the application as a whole will actually work with TicketOptions. It doesn't work with Attraction. Everything else is ticket-centric.

My problem is that TicketOption, in the context of creation I see it as a child of an aggregate. But, at the same time, in all other contexts it would make sense for it to be a root.

But if TicketOption has an ID and the other systems reference it. Then I would have an external reference to a child of an aggregate. Which doesn't sound right to me.

Is there any way to solve this problem?

5
  • 2
    your explanation is quite complex but im not sure any of your objects are ARs. It would be easier is you just told us what the products are and what the problem is?
    – Ewan
    Nov 8, 2023 at 21:10
  • @Ewan Thank you for reading , I have edited it trying to explain it. My problem is the creation of entities that from one point of view are independent but, on the other hand, together they comply with certain rules.
    – Tobías
    Nov 8, 2023 at 22:51
  • better, but still not clear. You don't explain the application you are creating "the creation of ticket types"? why cant the attraction just make a free text list of the types of ticket? presumably if there is some framework in your application for handling say "booking slots for guided tours" then you app can only handle the stuff its programmed for, you cant just make up new rules?
    – Ewan
    Nov 8, 2023 at 23:24
  • "the creation of ticket types" is a good name. I have added a second edition that I hope will explain it a little more.
    – Tobías
    Nov 9, 2023 at 6:14
  • Thank you for your attention, I have rewritten it from scratch. I hope it is now clear. You may have already noticed that I am not a native English speaker and it is quite difficult for me to express myself. Thanks.
    – Tobías
    Nov 9, 2023 at 8:36

2 Answers 2

4

"Don't play with another aggregate root's child entities" is a rule that is overrated because people tend to overuse aggregates and thus over-apply it:

  • First of all, if we're talking about a back-office/administration interface with low to moderate complexity and low collaboration, DDD's tactical patterns (aggregate, repository...) don't necessarily make sense there. Sometimes a simple CRUD is just good enough.

  • Aggregate is a write (command)-side pattern. The read side of your application is allowed to manipulate Ticket Options without going through their parent Attraction.

  • My problem is that TicketOption, in the context of creation I see it as a child of an aggregate. But, at the same time, in all other contexts it would make sense for it to be a root.

    The interesting word here is "context". You might have discovered that there are multiple bounded contexts inside the Product domain. It's perfectly fine for an entity to be a child in one bounded context's model and (its homonym) to be a root or a value object in another.

  • Be careful about reference data <-> production data, or Template <-> Instance business rules. You seem to say that if a ticket (Template) changes to AdultsOnly, sold tickets (Instances) might have to be invalidated. I would recommend against Template aggregates containing Instance children: in this case don't make Attraction the parent of SoldTickets. This type of rule is better handled with eventual consistency than inside the same aggregate because it potentially affects a lot of entities. SoldTicket is also probably already too big an aggregate root of itself with its own ivariants, etc. to become the child of another entity.

7
  • Thanks for your reply. [Published]TicketOption is something that is for sale. Tickets actually sold, will end up in another context with the information copied from what was sold. Tickets already been sold cannot be invalidated. With "invalidate" I mean remove from market. You may be right that DDD doesn't make sense here. But the whole is more complex, I tried to simplify it as much as possible here. There are quite a few rules to follow how things can be modified. There will be a Back, but also an API so that third parties create tickets. So I think that maybe a CRUD will not be adequate.
    – Tobías
    Nov 9, 2023 at 13:27
  • Can you provide examples showing why in all other contexts it would make sense for it to be a root? I'm assuming you're talking about modification here (again, reads are not a problem if you apply some sort of command/query separation). Why do you think it would be impractical or maybe poorly performant to go through the Attraction to modify one of its TicketOptions? Nov 9, 2023 at 14:12
  • Your question makes me think I'm using the wrong word. In other contexts TicketOption can't be modified. Part that manages prices needs to have something to assign prices to. It is done at ticket level, each ticket has a price depending on some conditions (I don't have all the rules). But I know they're ticket centric. They are not interested in the activity as a whole and how to manage tickets. Only need to know that ticket exists and some data: attraction/option name, show time, lang, etc. Always read-only. Aggregate Root is probably not the right word. It's a "read only entity" maybe?
    – Tobías
    Nov 9, 2023 at 14:47
  • I'm not concerned about performance at this stage, it's simply that I think the rest of the teams should not have access to Attraction. Because they will never need or be able to modify the Attraction and available TicketOptions. They only neead a partial view of TicketOption to read data.
    – Tobías
    Nov 9, 2023 at 15:00
  • I don't know if you're familiar with CQRS, but maybe you should take a look at that. Distinguishing between writes and reads in your types is often beneficial. Nov 9, 2023 at 15:06
3

OK I think I understand your problem.

You have:

Attraction
{
   List<TicketOption> TicketOptions
}

Where adding and removing ticket options has invariants,

But you also want

TicketOptionRepo.GetTicketOptionsAcrossMultipleAttractionsByFeature("child","tuesday")

I don't think this is a real issue as long as you just have read only methods for the TicketOptions.

Essentially your second call is a report on the stored data. You could enforce the readonlyness and add more separation by introducing a second type PublishedTicketOption or similar. It sounds like publishing would be a useful process to have anyway in this scenario where you might have new ticket types only available from date X, or "can't delete ticketoption as you have issues tickets of this type" validations

2
  • 1
    Yes, that's it :) PublishedTicketOption fits perfectly, maybe a ticket is created but it is not published immediately. PublishedTicketOption would be read-only, it would not modify the TicketOption from which it is derived. In this case, would it be fine for TicketOption to have a global ID just so that PublishedTicketOption can have a reference to it PublishedTicketOption.ticketOptionId == TicketOption.id? This way, the other services in the system will use PublishedTicketOption.id to define prices, quotas, etc.
    – Tobías
    Nov 9, 2023 at 12:22
  • lol "yes thats it!".. marks other answer as accepted
    – Ewan
    Nov 10, 2023 at 14:55

Your Answer

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

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