2
votes

I am currently working on a DDD system that is composed out of several bounded contexts. 2 of them are:

  1. Context "account management": Only staff members are allowed to work here. The idea is to manage customer accounts (like address, phone numbers, contacts etc etc) and to verify the account of a customer (basically checking if the data the customer supplied is valid).
  2. Context "website": I can login as a customer and edit my data (change my address for example)

Here is the issue:

A user logged in into the account management context is per definition an employee. So I can assume that changes made here are "trustworthy" in the sense of "the data is verified". A simplified variant of the appservice looks like this:

class AccountAppService
{
    public function changeAddress(string $accountId, string $address) : void
    {
        $account = $this->accountRepository->ofId(new Guid($accountId));
        $account->changeAddress(new Address($address));
    }
{

This is the appservice I am calling when an employee is changing an address. Note that there is no IdentityService that I inject/use in order to know who the employee is as this is not interesting here. The Account entity would emit an AccountAddressChanged event after successfully calling its changeAddress() method like so

class Account implements Entity
{
    public function changeAddress(Address $address) : void
    {
        $this->address = $address;
        DomainEventSubscriber::instance()->publish(new AccountAddressChanged($this));
    }
}

But I also need to reflect changes as soon as a customer edits data on the website. I plan to do this async via events a la "AccountAddressChangedViaWebsite". The account management context will subscribe and handle that event, setting the corresponding account to "unverified" again. So a simplified subscriber of the account management context could look like:

class AccountAddressChangedViaWebsiteSubscriber
{
    public function handle(AccountAddressChangedViaWebsite $event) : void
    {
        $accountId = $event->accountId();
        $address = $event->getAddress();
        $this->accountService->changeAddress($accountId, $address);
    }
}

Now the question: Employees call the appservice directly, customers via subscribers. If we say "we have to reverify an account after the customer updates his data" it sounds like a domain concept. Domain concepts fit into entities or domain services, but not into application services or subscribers for what I know. It implies to me that the following should be avoided (note the last line calling unverifyAccount()):

class AccountAddressChangedViaWebsiteSubscriber
{
    public function handle(AccountAddressChangedViaWebsite $event) : void
    {
        $accountId = $event->accountId();
        $address = $event->getAddress();
        $this->accountService->changeAddress($accountId, $address);
        $this->accountService->unverifyAccount($accountId);
    }
}

This is domain logic that is somewhat hidden in a subscriber which seems odd. I have the gut feeling that this should be the responsibility of a domain service, but how would the domain service know that it is called by an external event (via subscriber) or a command?

I could pass a sort of "Originator" ValueObject that tells me wheter the user causing this is an employee or an external system. Example:

class OriginatorService
{
    public function changeAddress(Originator $originator, Account $account, Address $address) : void
    {
        $account->changeAddress($address);
        if(($originator instanceof Employee) === false) {
            $account->unverify();
        }
    }
}

Here I delegate the responsibility of what to do to a domain service. But might double dispatching the OriginatorService into the Account entity be a good solution? This way the entity could check who caused the change via asking the passed in originatorService and could unverify itself.

I guess I am going down the DDD rabbit hole here, but what are your experiences/best practises in such a case?

1

1 Answers

2
votes

The simplest answer is probably introduce UnverifiedAddress as a concept in your model, rather than trying to treat "Address" as a universal idea with the verification bolted on as an afterthought.