Adding to list of values

I have an app where I need to keep adding new contacts to a list of contacts that belong to an association.

This list of contacts is part of the Association template. I have a choice where I pass the new contact as param, and “create this with contacts = contact::contacts”, thus adding the new contact to the list.

Of course this creates a new Association contract and archives the old one, which is perfectly fine. But is there another way to do this instead of potentially creating lots of archived Association contracts with almost the same data. Given that I will have hundreds of associations, I am trying to see if there is another approach that I should follow.

Any alternative design ideas will be appreciated. Thank you!

3 Likes

First, I’m going to assume you need access to the member list for some workflow, otherwise you may as well store a reference to the Association in the Member contract and avoid the list update completely.

Most ledger applications shouldn’t need to store archives, so thrashing the association list shouldn’t normally be an issue. Still, if you are updating the list sufficiently often the resulting contention may become an issue you need to resolve. In that case there are a few ways you can ameliorate problem.

Hundreds of associations is unlikely to be an issue, as I assume their relationship to Member contracts is 1:N not M:N. Only in the latter case would you start seeing contention on the ledger. A few thousand contracts in independent workflows is not really something you should be too worried about. If the Association contract is particularly large, then you might consider splitting the contract in two, linked by the contract id of the primary Association contract. This is called a contract constellation, where you have a single logical contract split into multiple concrete contracts unrelated to the underlying data model (ie. privacy, performance, storage scalability, etc).

daml 1.2
module BatchUpdateDaml
where

template Association
  with
    organiser : Party
    bigdata : Text
  where
    signatory organiser

    controller organiser can
      postconsuming DoUpdate : ContractId Association
        with
          newdata : Text
        do
          (membersId, members) <- fetchByKey @AssociationMembers (organiser, self)
          newAssociation <- create this with bigdata = newdata
          exercise membersId UpdateAssociation with newAssociation
          pure newAssociation

template AssociationMembers
  with
    organiser : Party
    association : ContractId Association
    members : [Text]
  where
    signatory organiser
    key (organiser, association) : (Party, ContractId Association)
    maintainer key._1

    controller organiser can
      UpdateAssociation : ContractId AssociationMembers
        with
          newAssociation : ContractId Association
        do
          create this with association = newAssociation

      UpdateAssociationMembers : ContractId AssociationMembers
        with
          newMember : Text
        do
          fetchByKey @Member (organiser, newMember)
          create this with members = newMember :: members

template Member
  with
    organiser : Party
    name : Text
  where
    signatory organiser
    key (organiser, member) : (Party, Text)
    maintainer organiser

This becomes slightly more complicated if the membership list changes often enough that you experience contention over the list updates. In this case you should consider reifying the membership updates and batching them.

daml 1.2
module BatchUpdateDemo
where

import DA.Optional

template Association
  with
    organiser : Party
    bigdata : Text
  where
    signatory organiser

    controller organiser can
      postconsuming DoUpdate : ContractId Association
        with
          newdata : Text
        do
          (membersId, members) <- fetchByKey @AssociationMembers (organiser, self)
          newAssociation <- create this with bigdata = newdata
          exercise membersId UpdateAssociation with newAssociation
          pure newAssociation

template AddMemberRequest
  with
    organiser : Party
    association : ContractId Association
    member : Text
  where
    signatory organiser

    controller organiser can
      Validate : Optional (ContractId Member)
        with
          org : Party
          ass : ContractId Association
        do
          assert $ org == organiser
          assert $ ass == association
          lookupByKey @Member (organiser, member)

template AssociationMembers
  with
    organiser : Party
    association : ContractId Association
    members : [Text]
  where
    signatory organiser
    key (organiser, association) : (Party, ContractId Association)
    maintainer key._1

    controller organiser can
      UpdateAssociation : ContractId AssociationMembers
        with
          newAssociation : ContractId Association
        do
          create this with association = newAssociation

      UpdateAssociationMembers : ContractId AssociationMembers
        with
          newMembers : [ContractId AddMemberRequest]
        do
          memberCids <- catOptionals <$> mapA (\amr -> exercise amr Validate with org = organiser; ass = association) newMembers
          newMembers <- fmap (.name) <$> mapA fetch memberCids
          create this with members = newMembers <> members

template Member
  with
    organiser : Party
    name : Text
  where
    signatory organiser
    key (organiser, name) : (Party, Text)
    maintainer key._1

(Note as written the above has an easily corrected race-condition between updating the association list and updating the association data that can result in requests being stranded. Fixing it would have distracted from the batching itself).

Batching does require adding a DAML Trigger to pass all visible requests into UpdateAssociationMembers. It also means adding a member is no longer an atomic operation, and adds a full round-trip latency to the operation. So this is something you should do only once you know you have a contention problem you need to solve. Still this approach should scale comfortably up to a few hundred contending updates per second — of course if that is a sustained rather than burst rate, at that level your other workflows will probably be experiencing starvation, and handling that will require application specific engineering.

5 Likes

Thank you for the detailed answer @Andrae . This solved my problem.

1 Like