3

I'm refactoring a settlements module in a large healthcare application. I'm trying to follow DDD.

Let me give you a short and simplified description of the refactored code:

  1. A patient has an appointment during which they undergo several medical procedures. Every procedure has its price.
  2. The patient can pay money to the clinic (either for the procedures, or some medical products)
  3. When they pay for procedures, the procedures become accounted
  4. An invoice or a receipt is issued
  5. Some of the money the patient pays is assigned to the doctor

The points above basically define the main processes and my refactoring scopes processes 2-5.

The processes are more complicated then it seems (loads of domain logic). In order to decouple the logic I decided to treat every process as a small sub-system. And for every sub-system I created a separate aggregate. Let me present it with some pseudo-code:

// Patient pays for whatever they're charged for
class PatientForPayments : AggregateRoot {
   private Payment[] Payments
   public Pay(amount){...}
}

// You can use the payment to account procedures in an appointment
class Appointment : AggregateRoot {
   private Procedure[] Procedures
   public IsFullyAccounted(){...}
   public Account(payment){...} // use the payment to account as many procedures as possible with the money paid
}

// An invoice can be issued
class PatientForInvoices : AggregateRoot {
   private Invoice[] Invoices
   public IssueInvoice(payment, procedures[]){...}
}

// Settle the money with a doctor
class ProcedureForSettlements : AggregateRoot {
   private Payment Payment
   private Doctor Doctor
   private Procedure Procedure
   private decimal Value
}

I'm wondering what's the best way to orchestrate it.

  • Some of the steps are optional - they depend on user input (e.g. issuing an invoice)
  • There is still some temporal coupling (e.g. you cannot issue an invoice for a payment which doesn't yet exist)
  • Some of the operations should go together (e.g. when a procedure gets accounted, we should settle with the doctor)

For now I orchestrated it in a domain service, called directly form the UI (code behind in a legacy WinForms app). Some more pseudo-code:

class PatientPaymentDomainService {
   public void HandlePayment(bool isInvoiceNeeded, PaymentDto paymentDto){
       payment = CreatePayment(paymentDto)
       database.Save(payment)

       appointment = database.GetAppointment(paymentDto.AppointmentId)
       appointment.Account(payment)
       database.Save(appointment)

       if(isInvoiceNeeded){
          patient = database.GetPatient(paymentDto.PatientId)
          patient.IssueInvoice(payment)
          database.Save(patient)
       }

       for(procedureDto in paymendDto.ProceduresDtos) {
          doctorSettlement = CreateDoctorSettlement(procedureDto, payment)
          database.Save(doctorSettlement)
       }
   }
   ...
}

The code above definitely encapsulates some domain logic (the workflow), so I believe it belongs to the domain layer and I would not put it the ViewModel / CodeBehind.

Putting everything in a single aggregate doesn't make much sense. I'd probably end up where I started - a huge blob of tangled and coupled logic.

However orchestrating it with domain events and handlers kind of makes sense.

What do you think? How would you model it?

1 Answer 1

3

First of all you need to identify the business rules and the consistency requirements for those rules. This leads to Aggregates' design. The Aggregate is the largest strong-consistency boundary. This means that, no matter what happens, an Aggregate is always consistent.

An Aggregate is something very nice but comes at a cost: it limits the scalability of the system. This means that one needs to make the Aggregates no larger and no smaller that they need to be.

Some rules can be enforced in an eventually consistent manner. Here come the Sagas/Process managers. A Saga ensures that the system as a whole will eventually be in a consistent state. A Saga models the business processes that usually span multiple Aggregates. It should be resumable/restartable: in case of machine failure, it can be restarted and it should resume from where it left off. A Saga is the inverse of an Aggregate. In a CQRS architecture, a Saga reacts to domain events and sends commands to the Aggregates. It can have state, which it uses to store the initial process data or to remember the process progress.

From DDD point of view, a Saga is a domain service: it is used to handle interactions between multiple Aggregates but it does not steal domain logic that should be placed in an Aggregate.

In your case, assuming that you have correctly found the Aggregate boundaries (you have correctly identified the Aggregates), you need a Saga to implement the Payment business process. It should be initialized with all the data it needs to start the process. In general case, a Saga should have 3 public methods:

  • init(data) - used to initialized the data
  • resume() - used to resume it after a failure
  • isEnded() - used by the system to know when to exclude a Saga instance from being run

The most interesting method is resume. It should have the following content:

if(!phaseOneEnded()) {
    processPhaseOne(); //it should send commands to the Aggregates in phase one
    markPhaseOneAsEnded(); //it changes its internal status and persist it
}

if(!phaseTwoEnded()) {
    processPhaseTwo(); //it should send commands to the Aggregates in phase one
    markPhaseTwoAsEnded(); //it changes its internal status and persist it
}

...

You should note that the Saga cannot know for sure that a phase is ended or not, and it can send the same commands multiple times. For example, if the machine is restarted after processPhaseOne() but before markPhaseOneAsEnded(). This means that the Aggregates should be able to detect a duplicate command and ignore it.

In your exact case, the Saga could look like this:

class PatientPaymentSaga {
   public static void init(bool isInvoiceNeeded, PaymentDto paymentDto, Persistence sagaPersistence){
      var saga = new PatientPaymentSaga( isInvoiceNeeded,  paymentDto);
      sagaPersistence.persist( saga );
   }


   public void resume(){
       if(!paymentCreated()){
          createAndSavePayment(this.paymentDto);
       }

       if(!appoimentAccounted()){
          accountAppointment(this.paymentDto.AppointmentId);
       }

       if(isInvoiceNeeded && !invoiceIssued()){
          issueInvoice(this.paymentDto);
       }


       for(procedureDto in paymendDto.ProceduresDtos) {
          if(!isDoctorSettledForPayment()){
              settleDoctor(procedureDto, this.payment);
          }

       }
   }

   public function isEnded()
   {
       return paymentCreated() && (!isInvoiceNeeded || invoiceIssued()) && doctorsSettled();
   }
   ...
}

How Sagas are run depends a lot of the programming language/framework. The simplest way is to have a cron job that loads all the non-ended Sagas and call saga.resume() for each.

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.