1
votes

I want to write my first application (e-commerce) in a DDD manner and I'd like to know whether I'm getting everything right so I want your opinion on my modelling - how can it be improved, what should I look for etc. Every feedback will be greatly appreciated.

I've divided my application into several bounded contexts (catalog, shopping, ordering) and below example is based on the shopping one. I am trying to use CQRS as well (thus AddProductToCart command).

My business requirements are as follows:

  • Customer should be able to add product to his cart
  • Price for added product should be specific to user and is based on several factors: (global discounts, user discounts and customer country (some countries have lowered base prices)

I've recognised following actors (please note comments on methods and classes):

/**
 * A customer (Aggregate Root) having an unique cart
 */
class Customer
{
    /**
     * @var int
     */
    private $customerId;

    /**
     * @var string
     */
    private $country;

    /**
     * @var Cart
     */
    private $cart;

    /**
     * @var PriceProvider
     */
    private $priceProvider;

    /**
     * Adds product to customers cart with user-specific price
     *
     * @param $productId
     * @return CartLine
     */
    public function addProductToCart($productId)
    {
        $price = $this->priceProvider->priceForProduct($productId, $this->customerId, $this->country);
        return $this->cart->addLine($productId, $price);
    }
}

/**
 * Simple CartLine object for persisting purposes
 */
class CartLine
{
    public $productId;
    public $price;
    public $cartId;

    function __construct($cartId, $productId, $price)
    {
        $this->cartId    = $cartId;
        $this->price     = $price;
        $this->productId = $productId;
    }
}

class Cart
{
    private $cartId;

    public function addLine($productId, $price)
    {
        return new CartLine($this->cartId, $productId, $price);
    }
}

/**
 * Provides price for specific country
 */
class PriceProvider
{
    public function priceForProduct($productId, $userId, $country)
    {
        // Logic for determining product price for customer
        // Based on available global discounts, user discounts and country
    }
}

/**
 * Command for adding product to cart
 */
class AddProductToCart
{
    public $customerId;
    public $productId;
}

/**
 * An application service to bind everything together
 */
class CustomerService
{
    public function addProductToCart(AddProductToCart $command)
    {
        /** @var Customer $customer */
        $customer = $this->customerRepository->customerOfId($command->customerId);
        $cartLine = $customer->addProductToCart($command->productId);

        $this->cartLineRepository->save($cartLine);
    }
}

Is this a correct approach? Do I violate any DDD principles here? Can I improve something?

2
You need an event too: AddProductToCartCommand-->ProductAddedToCartEvent - MeTitus
Just out of curiosity, Is this intended to be a learning app or a production app ? - Sudarshan
@Sudarshan, It might end up being small production one. ;) - acid
Why do you ask? Should I change something? - acid

2 Answers

1
votes

CQRS generally involves command handler.
Command is associated to "imperative" verb.
CustomerService is too broad and so does not represent the intent of the "command".

I would change CustomerService by AddProductToCart:

class AddProductToCart
{
    public function handle(AddProductToCartCommand $command) {
      ...
    }
}

By the way, by splitting all your various functionalities into appropriate commands, you would be able to make several versions of one specific command them if you need to.

0
votes

So in sparse order (and partially depending on the programming language):

  • Your aggregate root should be explicitly marked (using an IAggregateRoot interface for example) so if your language supports generic, you can implement only repository of an aggregate root.
  • Your constructors should be always private, use factory instead. Use constructor only for initialization issue. In your next future the building rule of a Customer could increase in complexity and factory will permit you flexibility.
  • Invariant logic is Domain Logic. Logic that depends on use case is Application Logic (i.e. PriceProvider is not related to domain logic)