Integration event handling logic (orchestration vs domain logic)
Asked Answered
A

1

1

I have an application that tracks the state of external documents locally through integration events. Some external document types aren't shared and some are. When handling an external event such as DocumentTypeChanged I have to execute logic similar to:

internalTypeId = internalDocumentTypeFrom(event.newTypeId);
shared = isSharedType(event.newTypeId);
if (internalTypeId == UNKNOWN && shared) trowMissingSharedDocTypeMappingError();

if (documentTracked(event.documentId)) {
  changeDocumentType(event.documentId, internalTypeId, event.id);

  if (!shared) removeDocument(event.documentId, event.id);
} else if (shared) {
  applicationId = mostRecentApplicationIdOfBn(event.businessNumber)
  attachDocument(applicationId, event.documentId, event.id, ...);
}

External documents are always supposed to change in reaction to external events only and the ExternalDocument AR always holds onto the latest externalEventId for that document.

Where should I place the above logic? Right now the logic is all in the integration's event handler and delegates commands such as changeDocumentType to the application's layer, but I feel most of the BL is creeping out of the domain.

When looking at IDDD samples, most integration handlers such as this one are translating events to application's service layer commands. It's a bit what I currently have, but I feel that poorly communicates the fact that these commands can only happen in reaction to external events.

Another idea I had was to do something like externalDocumentAppService.handleDocumentTypeChange(externalDocumentId, externalTypeId, externalEventId, bn, occurredOn) and then the application's service method may look like:

class ExternalDocAppService {
    handleDocumentTypeChange(externalDocumentId, externalTypeId, externalEventId, bn, occurredOn) {
        internalTypeId = internalDocumentTypeFrom(externalTypeId);
        shared = isSharedType(event.newTypeId);

        document = documentRepository.findByExternalId(externalDocumentId);

        if (document) {
            document.handleTypeChange(internalTypeId, shared, externalEventId, occurredOn);
        } else if (shared) {
            application = caseRepository.mostRecentApplicationOfBn(bn);
            document = application.attachDocument(externalDocumentId, ...);
        }

        if (document) documentRepository.save(document);
    }
}

class ExternalDocument {
    …
    handleTypeChange(internalTypeId, shared, externalEventId, occurredOn) {
        if (internalTypeId == UNKNOWN && shared) trowMissingSharedDocTypeMappingError();

        this.typeId = internalTypeId;
        this.latestExternalEventId = externalEventId;
        this.latestExternalChangeDate = occurredOn;

        if (!shared) this.remove(...);
    }
}

Looking at the code there would still be some business logic in the application's layer, such as knowing to ignore new unshared documents. Could we justify extracting the application's service logic in a domain service here, where the application service would simply delegate to the domain service (more or less)?

I'm also unsure about reactive command names such as handleTypeChange. Would something like changeTypeBecauseOfExternalTypeChange be more suitable? Anyway, I'm looking for some design guidance here… thanks!

Ardine answered 6/11, 2020 at 2:45 Comment(2)
Have you tried introducing a finite state machine, so that you have a place to put orchestration logic that is separate from your "domain" logic?Ferdinande
@Ferdinande Well, I have a place to put the orchestration logic, I can put it in the application's layer or in the integration event handler. I'm more concerned about not leaking domain logic into those.Ardine
F
1

My suggestion would be to introduce a finite state machine so that the boundary between "retrieving information" and "consuming information" is clear

class ExternalDocAppService {
  handleDocumentTypeChange(externalDocumentId, externalTypeId, externalEventId, bn, occurredOn) {

    // These might belong inside the finite state machine?
    internalTypeId = internalDocumentTypeFrom(externalTypeId);
    shared = isSharedType(event.newTypeId);

    fsm = new FSM(internalTypeId, shared, externalEventId, occurredOn);
    fsm.onDocument(
      documentRepository.findByExternalId(externalDocumentId)
    )

    if (fsm.needsApplication()) {
      fsm.onApplication(
        caseRepository.mostRecentApplicationOfBn(bn)    
      )
    }

    document = fsm.document();
    if (document) documentRepository.save(document);
  }
}

Which is to say, the process of handling this event has its own little state machine to help keep track of what information needs to be fetched and what information needs to be stored.

Notice that this state machine is really a short lived thing - it's managing some bookkeeping for the handling of this specific event. Once we're done with it, its state can be thrown away (contrast this with the state machines in our domain model, which are managing our domain dynamics, where we copy the state into our persistence store so that we can resume later).

Ferdinande answered 6/11, 2020 at 14:22 Comment(3)
Interesting idea, although I'm not sure how that's fundamentally better than a domain service encapsulating the entire logic block TBH. A big concern of my question is that often, with integration events, the event is transformed in an application layer COMMAND, named as such. E.g. here rather than handleDocumentTypeChange it would be changeDocumentType. I feel like with the former we make it implicit that the change is reactive: the application logic is responsible for deciding how to react to the change.Ardine
With the latter, we treat the external events as actors (e.g. users, console app) that would mutate the system, but the integration event handler has to make the translation of external events to local commands and therefore has to decide what these events meant for the domain. I almost always see the latter approach, but it just feels wrong to me and feel that appService.handleXHappened or appService.applyXHappened is more natural. Given no one seems to be doing that I'm wondering if I'm just misinterpreting something...Ardine
I absolutely agree with you! Integration event handlers belong in the application and possibly even in the domain layer, as they may apply domain logic in order to react properly to the event. Adapter layer components should neither contain application nor domain logic. They just adapt between technical media or act as anti-corruption-layer between contexts.Swinish

© 2022 - 2024 — McMap. All rights reserved.