IMHO you should start by getting the model right first. I would not expect a
Call to occur in more than one
CallQueue at a time, so this is just a
1:M relationship. Moreover, since an
Agent can only server one call at a time (as you wrote), this is a
1:1 relationship. The only
M:M relationship I see is the one between agents and queues.
You main question, however, stays the same (except in your option 1 there is only one link class required, not three). The answer to this boils down to which level of persistence ignorance you want or need to achieve.
For a fully persistence-ignorant model, you can try to model a
Call without any dependencies to a
CallQueue entity and without any dependencies to an
Agent entity. This is mainly your option 2. For example, the queues will reference their calls by some internal lists, the Agents will have a reference to the Call they currently serve, and agents and queues can reference each other by lists as well. Of course, if you need additional attributes for the link between agents and queues, a class like
AgentToCallQueueLink will be required.
One advantage of this is avoiding cyclic dependencies (for example, between Calls and CallQueues), which makes things like unit testing a little bit easier.
However, persistence ignorance does not come for free:
achieving persistence is somewhat harder
working with isolated subsets of the data (units of work) is harder, since this implicitly assumes that there is a persisting storage in the background holding the data outside the current unit of work
getting the transactions correct in a concurrent environment is somewhat harder (especially if you don't want to add CQRS to your system)
For example, by modeling a
Call object with a primary key
CallID and a foreign key attribute
CallQueueID, knowing it will be stored that way in a database, it will become a little bit easier to pull a some Call objects of a queue from the database, manipulate the queue in isolation and update the original
CallQueue content without the necessity to delete and rewrite all the calls of the queue. However, now you have introduced a cyclic dependendency between
Calls in your domain model (which can be perfectly acceptable, since actually it is just a weak dependency by IDs, not more).
In the past, I have often used both: classes with all foreign key ids as attributes in a 1-to-1 correspondence to the columns in the database, and additionally lists of references, which allows to write more concise code. This is redundant information, of course, but this redundancy is fine as long as the keys are immutable and changing a reference to another instance is either forbidden or properly encapsulated.
So in the end, it is a trade-off. If you need persistence, and don't need persistence ignorance, go with option 1. A repository per type will be then ok. Your mapping from the domain model to a database will be simpler. And if you consider to use an ORM, this will be a natural fit (if you model your classes directly in a way they fit to the restrictions the ORM imposes on them).
However, if you think you really need persistence ignorance, go with option 2, but don't be astonished if persistence, dealing with overlapping units of work and transactional handling will become harder. Option 2 is often a better fit for models where either no persistence is required, or the only way of required persistence is to load and save all the data of an aggregate at once, with no concurrent writers. For this usage, one repo for loading and saving everything might be ok.