2

I have a domain layer, which my application architecture holds "sacred"; that is to say, the domain layer has no references to either data storage, or presentation concerns (vice-versa is allowed). I would like to keep it this way!

Everything was going quite well until I came to one particular requirement. Given:

public class A
{
     public string IdA { get; private set; }
     public void DoWork()
}

public class B
{
    public string IdB { get; private set; }
    public string IdA { get; private set; }
    public Invaidate()
}

Under certain uncommon conditions, which A decides, when DoWork is called all associated B must be invalidated. The problem, this invalidation requires persistence. Note: this edge case is the only time A would need to be concerned with its associated Bs. The requirement for this invalidation would not be obvious to a consumer of A.

I have come up with, and implemented, a solution (1), as well as thought of other solutions. However, I am not quite happy with it, or any of the others, and would like input.

Solution 1: A Invalidates Bs, Calls Delegate For Persistence

Advantage: Transparent to consumer, Easy to consume

Disadvantage: Strange and inconsistent save behavior, no other class in the domain layer saves when a method is called

class A
{
    private Action<B> SaveB; // Populated by ARepository when instance is retrieved, delegate points to sister repo BRepository
    public List<B> Bees { get; private set; }
    public DoWork()
    {
        // if edgeCase:
        foreach (B b in Bees)
        {
            b.Invalidate();
            SaveB(b);
        }
    }
 }

Solution 2: A Invalidates Bs, A's Repository Saves Associated Bs

Advantage: Transparent to consumer, Easy to consume, Consistent Domain Layer

Disadvantage: Inconsistent persistent layer, Monolithic Persistence Antipattern*

*Perhaps the wrong anti-pattern name?

class A
{
    public List<B> Bees { get; private set; }
    public DoWork()
    {
        // if edgeCase:
        foreach (B b in Bees)
            b.Invalidate();
    }
}
//...
class ARepository
{
     Update(A a)
     {
         Sql.SaveA(a);
         bRepository.Update(a.Bees);
     }
}

Solution 3: Trigger Event On Edge Case

Advantage: No inconsistencies in domain or persistence layer behavior, No cross entity concerns

Disadvantage: Hard to consume, Exposes logic to consumer, who is supposed to be responsible for wiring up the event?!

class A
{
    public Event SaveBs/EdgeCaseHappened;
    public List<B> Bees { get; private set; }
    public DoWork()
    {
        // if edgeCase:
        foreach (B b in Bees)
            b.Invalidate();
        SaveBs.Fire();
       // or just...
       EdgeCaseHappened.Fire()
    }
}

While I have already implemented 1, none of these three solutions seem quite right to me, is there something here that I am missing? Have I made a faulty assumption somewhere?

What is the best course of action for me too keep cross entity concerns like this maintainable and usable, while strictly isolating my business logic?


Note: this is not a DDD project

1

It depends on whether Bs must be invalidated right away when A's edge case happened, or not.

If yes -- I'd try to make the project more DDD-like, at least on tactical level. In that case A would be an aggregate root, and Bs would hide behind an aggregate boundary. So all you need is to merge the responsibilities of ARepository and BRepository.

If not -- I would implement something like Saga, though I'm not aware of your specifics. It's closest to your Solution 3. Since, again, I'm not aware of your semantics, I'll try to put it abstractly. Say, you have some process consisting of several steps -- hence, we have eventual consistency. One of that steps, say, A::doWork(), could go wrong. So it requires some rollback logic to be implemented. You fire an event in that case, which is listened by some controller, who delegates the rollback domain logic to A.Bees, so each B::invalidate() can be invoked. Since you know in advance what business-logic is invoked, you know what repository you need -- BRepository. Probably this solution is easier to implement if you invert the references: if it's not A who knows its Bs, but Bs know about their A.

  • While this is a good well thought out answer, it seems to show a DDD bias XD What I don't like about DDD is that it is a one size fits all architecture guide imo. In DDD you avoid tricky situations like the one in my question, but you greatly reduce reusibility as well. – TheCatWhisperer Dec 11 '17 at 13:24
  • Um wow. I've never encountered such opinion. What do you mean by "one size fits all architecture guide"? Do you imply the prescriptive use of aggregates, entities, value objects, etc? If yes -- it's nothing but a decent OOP practices, resulting in simplicity and code maintainability. And this is (should be imo) a goal of any developer. And on that reusability thing -- it was never a goal of OOP. It's a fallacy (God forbid!) – Zapadlo Dec 11 '17 at 13:38
  • I mean, it works for almost any project, but also, generally the same structure. You just form your aggregate routes, and put the value objects around it. You don't have to deal with nuanced issues like this. I don't really care if reuse is a goal of OOP or not, it is a goal for me. My project would be huge if I used proper DDD. – TheCatWhisperer Dec 11 '17 at 13:51
  • 1
    Huge and maintainable :) – Zapadlo Dec 11 '17 at 13:55
  • Personally, I feel maintainability and amount of code are inversely related. Yes, the advantages of DDD prbly more than offset the extra code by isolating the different functions of a system. However, given in exchange, are the ability to rapidly add new features by leveraging existing structures. – TheCatWhisperer Dec 11 '17 at 14:03

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.