Does my code follow DDD principles?

I'm trying to model P2P crypto trading using DDD and I'm having trouble modeling the lifecycle of sell orders. Lifecycle: Some trader-seller creates an order. Some trader-buyer responds to it. Buyer confirms the transfer of fiat to the seller's account. Seller confirms the receipt of fiat from the buyer to his account. As a result, I got the following C# code: SellOrder.cs: public class SellOrder { public SellOrder(Guid guid, SellOrderInfo info, Guid sellerGuid, string sellerToExchangerTransferTransactionHash) { if (!ValidateSellOrderInfo(info)) throw new InvariantViolationException("Info is invalid."); if (string.IsNullOrWhiteSpace(sellerToExchangerTransferTransactionHash)) throw new DevelopmentErrorException("Seller to exchanger transfer transaction hash is invalid."); Status = SellOrderStatus.Created; SellerGuid = sellerGuid; SellerToExchangerTransferTransactionHash = sellerToExchangerTransferTransactionHash; BuyerGuid = null; BuyerWalletAddress = null; } private bool ValidateSellOrderInfo(SellOrderInfo info) { // Something } public void Respond(Guid buyerGuid, string buyerWalletAddress) { if (Status != SellOrderStatus.Created) throw new InvariantViolationException("Status is invalid."); if (string.IsNullOrWhiteSpace(buyerWalletAddress)) throw new DevelopmentErrorException("Buyer wallet address is invalid."); BuyerGuid = buyerGuid; BuyerWalletAddress = buyerWalletAddress; Status = SellOrderStatus.BuyerResponded; } public void Confirm(Guid traderGuid) { Status = Status switch { SellOrderStatus.BuyerResponded when traderGuid.Equals(BuyerGuid) => SellOrderStatus.BuyerConfirmed, SellOrderStatus.BuyerResponded => throw new InvariantViolationException("Trader is not a buyer."), SellOrderStatus.BuyerConfirmed when traderGuid.Equals(SellerGuid) => SellOrderStatus.BuyerAndSellerConfirmed, SellOrderStatus.BuyerConfirmed => throw new InvariantViolationException("Trader is not a seller."), _ => throw new InvariantViolationException("Status is invalid.") }; } public SellOrderStatus Status { get; } public Guid Guid { get; } public SellOrderInfo Info { get; } public Guid SellerGuid { get; } public string SellerToExchangerTransferTransactionHash { get; } public Guid? BuyerGuid { get; private set; } public string? BuyerWalletAddress { get; private set; } } As I understand it, the Trader and SellOrder entities are separate aggregation roots. In the SellOrder class, I initially stored references directly to the Trader class objects, but then I learned that aggregation roots should not reference each other directly, and replaced the objects with identifiers. Did I do the right thing? I also have a question about the void Confirm(Guid traderGuid) method: does it violate any DDD principles that I check the trader on whose behalf this method is called? Maybe it would be better to move this check to the application layer or to some domain layer service? I also plan to add a dispute system - if the seller does not confirm receipt of the fiat, a dispute should be automatically created referencing this order. What is the best way to implement this? I think domain events would be the perfect solution here.

Feb 11, 2025 - 12:29
 0
Does my code follow DDD principles?

I'm trying to model P2P crypto trading using DDD and I'm having trouble modeling the lifecycle of sell orders. Lifecycle:

  1. Some trader-seller creates an order.
  2. Some trader-buyer responds to it.
  3. Buyer confirms the transfer of fiat to the seller's account.
  4. Seller confirms the receipt of fiat from the buyer to his account.

As a result, I got the following C# code:

SellOrder.cs:

public class SellOrder
{
    public SellOrder(Guid guid, SellOrderInfo info, Guid sellerGuid, string sellerToExchangerTransferTransactionHash)
    {
        if (!ValidateSellOrderInfo(info))
            throw new InvariantViolationException("Info is invalid.");
        if (string.IsNullOrWhiteSpace(sellerToExchangerTransferTransactionHash))
            throw new DevelopmentErrorException("Seller to exchanger transfer transaction hash is invalid.");
        
        Status = SellOrderStatus.Created;

        SellerGuid = sellerGuid;
        SellerToExchangerTransferTransactionHash = sellerToExchangerTransferTransactionHash;

        BuyerGuid = null;
        BuyerWalletAddress = null;
    }
    
    private bool ValidateSellOrderInfo(SellOrderInfo info)
    {
        // Something
    }

    public void Respond(Guid buyerGuid, string buyerWalletAddress)
    {
        if (Status != SellOrderStatus.Created)
            throw new InvariantViolationException("Status is invalid.");
        if (string.IsNullOrWhiteSpace(buyerWalletAddress))
            throw new DevelopmentErrorException("Buyer wallet address is invalid.");

        BuyerGuid = buyerGuid;
        BuyerWalletAddress = buyerWalletAddress;
        Status = SellOrderStatus.BuyerResponded;
    }

    public void Confirm(Guid traderGuid)
    {
        Status = Status switch
        {
            SellOrderStatus.BuyerResponded when traderGuid.Equals(BuyerGuid) => SellOrderStatus.BuyerConfirmed,
            SellOrderStatus.BuyerResponded => throw new InvariantViolationException("Trader is not a buyer."),
            SellOrderStatus.BuyerConfirmed when traderGuid.Equals(SellerGuid) => SellOrderStatus.BuyerAndSellerConfirmed,
            SellOrderStatus.BuyerConfirmed => throw new InvariantViolationException("Trader is not a seller."),
            _ => throw new InvariantViolationException("Status is invalid.")
        };
    }
    
    public SellOrderStatus Status { get; }
    
    public Guid Guid { get; }
    
    public SellOrderInfo Info { get; }

    public Guid SellerGuid { get; }

    public string SellerToExchangerTransferTransactionHash { get; }

    public Guid? BuyerGuid { get; private set; }

    public string? BuyerWalletAddress { get; private set; }
}

As I understand it, the Trader and SellOrder entities are separate aggregation roots. In the SellOrder class, I initially stored references directly to the Trader class objects, but then I learned that aggregation roots should not reference each other directly, and replaced the objects with identifiers. Did I do the right thing?

I also have a question about the void Confirm(Guid traderGuid) method: does it violate any DDD principles that I check the trader on whose behalf this method is called? Maybe it would be better to move this check to the application layer or to some domain layer service?

I also plan to add a dispute system - if the seller does not confirm receipt of the fiat, a dispute should be automatically created referencing this order. What is the best way to implement this? I think domain events would be the perfect solution here.