MVC and anaemic templates

A quick google search on the term “anaemic domain models” yields plenty of opinions.

Lately I’ve been curious about adopting an MVC style approach to structuring DAML code bases.

The tldr is,

  • templates have no choices, simply fields representing columns in a table.
  • service layer implements the business logic and is the main driver of stitching together the workflows that would normally occur via exercising choices directly within the domain model.

Some initial thoughts

pros:

  • if model and controller/service layer are in separate DAR’s then workflow changes don’t require templates to be upgraded and redeployed?

cons:

  • is it idiomatic daml?
  • service layer always has to remember to archive when updating a contract since you now opt out of auto archiving of consuming choices.

What’s everyone’s opinion?

1 Like

The pattern you describe of separating data and rules is one that has been debated at great lengths. I think it is a very good guiding principle and helps structure a project in such a way that upgrades become easier. It does, however, also increase the complexity slightly as transferring authority and managing visibility become a bit more involved.

Let’s imagine we have Cash signed by issuer and owner, without any choices. We now want to write a transfer rule from one owner to another, say from Alice and Bob. Let’s assume all “rules” (what you call services) are on a separate CashRules contract.

Who signs the CashRules contract? Is it bilateral between issuer and owners? If so, how does does one owner find out that another subscribes to the rules?
Does everyone sign one instance? Then all cash owners know about all others. by virtue of all being signatories on one contract. Is that acceptable?

Let’s assume cash owners should not know about each other until they do business. So Alice starts with a CashRules contract signed and visible only to Alice and Issuer.

At some point in the process, we need to get the authority of all three parties into scope. Without the separation that can be done with a TransferProposal signed by Alice and Issuer with a choice for Bob. But if we don’t allow ourselves a choice for Bob on the data (which in this case is Cash or TransferProposal, that doesn’t work. So Alice has to issue a side-contract for the CashRules that allows Bob to accept transfers. The issuer probably only wants Bob to be able to use that if he, too, subscribes to the rules. All this leads to something like this:

template Cash
  with
    issuer : Party
    owner : Party
  where
    signatory issuer, owner
    
template TransferProposal
  with
    issuer : Party
    owner : Party
    newOwner : Party
  where
    signatory issuer, owner
    observer newOwner

template CashRules
  with
    issuer : Party
    owner : Party
  where
    signatory issuer, owner

    controller owner can
      nonconsuming ProposeTransfer : ContractId TransferProposal
        with
          cash : ContractId Cash
          newOwner : Party
        do
          c <- fetch cash
          assert (c.owner == owner)
          assert (c.issuer == issuer)
          archive cash
          let tp = TransferProposal with ..
          r <- create tp
          oCid <- lookupByKey @TransferRules tp
          case oCid of
            None -> create TransferRules with ..
            Some cid -> return cid
          return r
      
      nonconsuming AcceptTransfer : ContractId Cash
        with
          cid : ContractId TransferProposal
        do
          tp <- fetch cid
          exerciseByKey @TransferRules tp TR_AcceptTransfer with ..

template TransferRules
  with
    issuer : Party
    owner : Party
    newOwner : Party
  where
    signatory issuer, owner

    key TransferProposal with .. : TransferProposal
    maintainer key.issuer, key.owner

    controller issuer, newOwner can
      nonconsuming TR_AcceptTransfer : ContractId Cash
        with
          cid : ContractId TransferProposal
        do
          tp <- fetch cid
          assert (tp == TransferProposal with ..)
          archive cid
          create Cash with issuer, owner = newOwner

template Setup
  with
    issuer : Party
    owner : Party
  where
    signatory issuer

    controller owner can
      Init : (ContractId Cash, ContractId CashRules)
        do
          c <- create Cash with ..
          cr <- create CashRules with ..
          return (c, cr)

t = scenario do
  [issuer, alice, bob] <- mapA getParty ["Issuer", "Alice", "Bob"]
  (init1, init2) <- submit issuer do
    init1 <- create Setup with owner = alice; ..
    init2 <- create Setup with owner = bob; ..
    return (init1, init2)
  (ac, acr) <- submit alice do
    exercise init1 Init
  (bc, bcr) <- submit bob do
    exercise init2 Init
  tp <- submit alice do
    exercise acr ProposeTransfer with
      cash = ac
      newOwner = bob
  bc2 <- submit bob do
    exercise bcr AcceptTransfer with
      cid = tp
  return ()  

That’s quite involved compared to the more basic approach. I therefore personally tend to keep “basic” choices like transfers on the templates themselves, and only build higher level functionality like Swaps in separate rule templates.

EDIT: For completeness, I added a Setup template and short scenario to demonstrate. As you can see, the final usage is just as easy as the simpler “integrated” approach, but there are twice as many templates and more involved choice bodies.

3 Likes