Domain-Driven Design (DDD) Overview

The Shirinzad platform implements Domain-Driven Design principles, placing business logic within rich domain entities rather than in services. This approach ensures business rules are encapsulated, testable, and maintainable.

Core Principle: Domain entities contain business logic and validation rules. Application services orchestrate these entities but don't contain business logic themselves.

Key DDD Concepts

Entities

  • Identity: Each entity has a unique identifier (Guid)
  • State: Encapsulates business data and rules
  • Behavior: Methods that change state following business rules
  • Validation: Ensures data integrity before persistence

Aggregate Roots

  • Consistency Boundary: Ensures transactional consistency
  • Access Control: Entry point for related entities
  • Invariants: Enforces business rules across aggregates
  • Examples: Product, Order, Cart, Wishlist

Domain Entity Statistics

55+
Domain Entities
15
Aggregate Roots
100+
Business Methods
80+
Validation Rules

Domain Model Structure

Entity Hierarchy

graph TB subgraph Base["ABP Framework Base Classes"] Entity[Entity<Guid>] AuditedEntity[AuditedEntity<Guid>] FullAuditedEntity[FullAuditedEntity<Guid>] AggregateRoot[AggregateRoot<Guid>] FullAuditedAggregateRoot[FullAuditedAggregateRoot<Guid>] end subgraph Catalog["Catalog Domain"] Product[Product] Category[Category] Brand[Brand] ProductReview[ProductReview] end subgraph Orders["Orders Domain"] Order[Order] Cart[Cart] Payment[Payment] Shipment[Shipment] end subgraph User["User Domain"] UserProfile[UserProfile] UserAddress[UserAddress] Wishlist[Wishlist] end Entity --> AuditedEntity AuditedEntity --> FullAuditedEntity Entity --> AggregateRoot AggregateRoot --> FullAuditedAggregateRoot FullAuditedAggregateRoot --> Product FullAuditedAggregateRoot --> Category FullAuditedAggregateRoot --> Order FullAuditedAggregateRoot --> Cart FullAuditedAggregateRoot --> Wishlist FullAuditedEntity --> UserProfile

Domain Entities by Module

Product Catalog Domain (19 Entities)

Product (Aggregate Root)

Purpose: Main product entity with pricing, stock, and metadata.

public class Product : FullAuditedAggregateRoot<Guid>
{
    // Properties
    public string Name { get; set; }
    public string Slug { get; set; }
    public decimal? Price { get; set; }
    public decimal? DiscountPrice { get; set; }
    public int? StockQuantity { get; set; }
    public bool IsActive { get; set; }
    public bool IsFeatured { get; set; }
    public int ViewCount { get; set; }
    public decimal AverageRating { get; set; }
    public int RatingCount { get; set; }

    // Business Methods
    public void DecreaseStock(int quantity)
    {
        if (quantity <= 0)
            throw new BusinessException("Quantity must be positive");

        if (StockQuantity < quantity)
            throw new BusinessException("Insufficient stock");

        StockQuantity -= quantity;
    }

    public void IncreaseStock(int quantity)
    {
        if (quantity <= 0)
            throw new BusinessException("Quantity must be positive");

        StockQuantity += quantity;
    }

    public void UpdateRating(decimal newRating)
    {
        // Recalculate average rating
        var totalRating = (AverageRating * RatingCount) + newRating;
        RatingCount++;
        AverageRating = totalRating / RatingCount;
    }

    public bool IsInStock() => StockQuantity.HasValue && StockQuantity > 0;

    public decimal GetEffectivePrice()
    {
        return DiscountPrice.HasValue && DiscountPrice > 0
            ? DiscountPrice.Value
            : Price ?? 0;
    }

    public bool IsDiscounted() => DiscountPrice.HasValue && DiscountPrice < Price;

    public decimal GetDiscountPercentage()
    {
        if (!IsDiscounted() || !Price.HasValue || Price == 0)
            return 0;

        return ((Price.Value - DiscountPrice.Value) / Price.Value) * 100;
    }

    public bool IsLowStock()
    {
        return LowStockThreshold.HasValue &&
               StockQuantity.HasValue &&
               StockQuantity <= LowStockThreshold;
    }
}

Key Entities

Entity Key Business Methods Business Rules
Product DecreaseStock(quantity)
IncreaseStock(quantity)
UpdateRating(rating)
GetEffectivePrice()
IsLowStock()
- Stock cannot go negative
- Slug must be unique
- Rating must be 1-5
- Discount price < regular price
Category SetParent(category)
HasChildren()
GetAncestors()
GetDescendants()
- Cannot be parent of itself
- Circular references prevented
- Slug must be unique
- Active parent required
ProductReview Approve(userId)
Reject()
AddReply(text, author)
MarkAsVerifiedPurchase()
- Rating 1-5 required
- Must have content
- Approved by admin
- One review per user per product
Wishlist AddItem(product, variant)
RemoveItem(itemId)
SetAsDefault()
MakePublic()/MakePrivate()
- One default wishlist per user
- Max 100 items per wishlist
- Cannot duplicate items
- Public wishlists shareable

Orders Domain (13 Entities)

Order (Aggregate Root)

Purpose: Customer order with complete order lifecycle management.

public class Order : FullAuditedAggregateRoot<Guid>
{
    public string OrderNumber { get; set; }
    public Guid? UserId { get; set; }
    public OrderStatus Status { get; set; }
    public decimal Subtotal { get; set; }
    public decimal DiscountAmount { get; set; }
    public decimal TaxAmount { get; set; }
    public decimal ShippingCost { get; set; }
    public decimal Total { get; set; }

    public ICollection<OrderItem> Items { get; set; }
    public ICollection<OrderStatusHistory> StatusHistory { get; set; }

    // Business Methods
    public void UpdateStatus(OrderStatus newStatus, Guid? changedByUserId, string notes = null)
    {
        // Validate status transition
        if (!CanTransitionTo(newStatus))
        {
            throw new BusinessException($"Cannot change order status from {Status} to {newStatus}");
        }

        var oldStatus = Status;
        Status = newStatus;

        // Add to history
        var history = new OrderStatusHistory
        {
            OrderId = Id,
            FromStatus = oldStatus,
            ToStatus = newStatus,
            ChangedAt = DateTime.UtcNow,
            ChangedByUserId = changedByUserId,
            Notes = notes
        };

        StatusHistory.Add(history);
    }

    private bool CanTransitionTo(OrderStatus newStatus)
    {
        return (Status, newStatus) switch
        {
            (OrderStatus.Pending, OrderStatus.Processing) => true,
            (OrderStatus.Pending, OrderStatus.Cancelled) => true,
            (OrderStatus.Processing, OrderStatus.Shipped) => true,
            (OrderStatus.Processing, OrderStatus.Cancelled) => true,
            (OrderStatus.Shipped, OrderStatus.Delivered) => true,
            (OrderStatus.Delivered, OrderStatus.Returned) => true,
            _ => false
        };
    }

    public void CalculateTotals()
    {
        Subtotal = Items.Sum(i => i.LineTotal);
        Total = Subtotal - DiscountAmount + TaxAmount + ShippingCost;
    }

    public void ApplyDiscount(decimal amount)
    {
        if (amount < 0 || amount > Subtotal)
            throw new BusinessException("Invalid discount amount");

        DiscountAmount = amount;
        CalculateTotals();
    }

    public bool CanBeCancelled()
    {
        return Status == OrderStatus.Pending || Status == OrderStatus.Processing;
    }

    public void Cancel(string reason)
    {
        if (!CanBeCancelled())
            throw new BusinessException("Order cannot be cancelled");

        Status = OrderStatus.Cancelled;
        // Restore product stock
        foreach (var item in Items)
        {
            // This would be done in domain service
        }
    }
}

Cart (Aggregate Root)

public class Cart : FullAuditedAggregateRoot<Guid>
{
    public Guid? UserId { get; set; }
    public string? SessionId { get; set; }
    public bool IsActive { get; set; }
    public DateTime ExpiresAt { get; set; }
    public decimal Subtotal { get; set; }
    public decimal DiscountAmount { get; set; }
    public decimal Total { get; set; }

    public ICollection<CartItem> Items { get; set; }

    public void AddItem(Guid productId, string productName, decimal price, int quantity)
    {
        if (quantity <= 0)
            throw new BusinessException("Quantity must be positive");

        // Check if item already exists
        var existingItem = Items.FirstOrDefault(i => i.ProductId == productId);
        if (existingItem != null)
        {
            existingItem.Quantity += quantity;
        }
        else
        {
            var newItem = new CartItem
            {
                CartId = Id,
                ProductId = productId,
                ProductName = productName,
                Price = price,
                Quantity = quantity
            };
            Items.Add(newItem);
        }

        CalculateTotals();
    }

    public void UpdateItemQuantity(Guid itemId, int quantity)
    {
        if (quantity <= 0)
            throw new BusinessException("Quantity must be positive");

        var item = Items.FirstOrDefault(i => i.Id == itemId);
        if (item == null)
            throw new BusinessException("Item not found in cart");

        item.Quantity = quantity;
        CalculateTotals();
    }

    public void RemoveItem(Guid itemId)
    {
        var item = Items.FirstOrDefault(i => i.Id == itemId);
        if (item != null)
        {
            Items.Remove(item);
            CalculateTotals();
        }
    }

    public void Clear()
    {
        Items.Clear();
        Subtotal = 0;
        Total = 0;
        DiscountAmount = 0;
    }

    public void CalculateTotals()
    {
        Subtotal = Items.Sum(i => i.Price * i.Quantity);
        Total = Subtotal - DiscountAmount;
    }

    public bool IsEmpty() => !Items.Any();

    public int GetTotalItems() => Items.Sum(i => i.Quantity);
}

User Domain (4 Entities)

Entity Key Business Methods Business Rules
UserProfile UpdateProfile(input)
SetAvatar(url)
ValidateNationalCode()
GetFullName()
- One profile per user
- National code must be valid (10 digits)
- Gender: Male/Female/Other
- Birth date must be in past
UserAddress SetAsDefault()
Validate()
GetFullAddress()
- One default address per user
- Postal code 10 digits
- Phone number required
- Province and city required
LoginAttempt RecordSuccess()
RecordFailure()
IsAccountLocked()
- Track all login attempts
- Lock after 5 failed attempts
- Auto-unlock after 30 minutes
- Store IP address and user agent

Automotive Domain (9 Entities)

Entity Key Business Methods Business Rules
Vehicle MarkAsSold()
MarkAsAvailable()
UpdatePrice(price)
AddService(serviceId)
- VIN must be unique
- Year must be valid (1900-current)
- Price must be positive
- Condition: New/Used
VehiclePart AddCompatibility(modelId, years)
RemoveCompatibility(modelId)
IsCompatibleWith(modelId, year)
- Part number must be unique
- Compatibility years valid
- Stock quantity >= 0
- Price must be positive
Service SetPrice(price)
SetDuration(minutes)
AddToVehicle(vehicleId)
- Price and duration required
- Category must be active
- Service name unique per category

Blog & CMS Domain (10 Entities)

Entity Key Business Methods Business Rules
BlogPost Publish()
Unpublish()
IncrementViewCount()
AddComment(comment)
AddTag(tagId)
- Slug must be unique
- Published date required for publish
- Author required
- Content max 50,000 chars
BlogComment Approve()
Reject()
Reply(text, userId)
- Must have content
- Requires moderation
- Parent comment validation
- Max depth: 3 levels
Page Publish()
Unpublish()
SetTemplate(template)
- Slug must be unique
- Template must exist
- Content required
- Title max 300 chars
Slider Activate()
Deactivate()
SetDateRange(start, end)
- Start date < end date
- Image URL required
- Location: Home/Category/Product
- Display order for sorting

Business Workflows

Order Lifecycle Workflow

stateDiagram-v2 [*] --> Pending: Create Order Pending --> Processing: Payment Confirmed Pending --> Cancelled: Payment Failed/User Cancel Processing --> Shipped: Shipment Created Processing --> Cancelled: Admin Cancel/Stock Issue Shipped --> Delivered: Delivery Confirmed Shipped --> Processing: Return to Processing Delivered --> Returned: Return Requested Delivered --> [*]: Order Complete Cancelled --> [*] Returned --> [*] note right of Pending - Cart converted to order - Stock reserved - Payment initiated end note note right of Processing - Payment verified - Stock deducted - Shipment prepared end note note right of Shipped - Tracking number assigned - Customer notified - In transit end note note right of Delivered - Customer received - Review invitation sent - Transaction complete end note

Cart to Order Conversion

sequenceDiagram participant Customer participant Cart participant Order participant Product participant Payment participant Notification Customer->>Cart: Add items to cart Cart->>Cart: Calculate totals Customer->>Cart: Apply discount code Cart->>Cart: Recalculate totals Customer->>Order: Checkout Order->>Cart: Get cart items Order->>Product: Check stock availability alt Stock Available Product-->>Order: Confirm stock Order->>Order: Create order items Order->>Product: Decrease stock Order->>Payment: Initiate payment alt Payment Success Payment-->>Order: Payment confirmed Order->>Order: Update status to Processing Order->>Cart: Clear cart Order->>Notification: Send confirmation email Notification-->>Customer: Email sent else Payment Failed Payment-->>Order: Payment failed Order->>Product: Restore stock Order->>Order: Update status to Cancelled Order->>Notification: Send failure notification end else Stock Unavailable Product-->>Order: Stock insufficient Order-->>Customer: Show error message end

Product Review Workflow

flowchart TD Start([Customer submits review]) --> Validate{Validation} Validate -->|Valid| CheckPurchase{Verified Purchase?} Validate -->|Invalid| Error[Show validation errors] CheckPurchase -->|Yes| MarkVerified[Mark as verified purchase] CheckPurchase -->|No| MarkUnverified[Regular review] MarkVerified --> SaveReview[Save to database] MarkUnverified --> SaveReview SaveReview --> Moderation{Auto-approve?} Moderation -->|Yes - High trust user| Approve[Approve review] Moderation -->|No - Needs moderation| Pending[Pending approval] Approve --> UpdateProduct[Update product rating] UpdateProduct --> NotifyUser[Notify customer] NotifyUser --> End([Review published]) Pending --> AdminReview{Admin reviews} AdminReview -->|Approve| Approve AdminReview -->|Reject| Reject[Reject review] Reject --> NotifyRejection[Notify customer] NotifyRejection --> End

Business Validation Rules

Product Validation Rules

Field Validation Rule Error Message (Persian) Error Code
Name Required, max 200 characters نام محصول الزامی است PRODUCT_NAME_REQUIRED
Slug Required, unique, max 250 characters, URL-friendly نام مستعار باید منحصر به فرد باشد PRODUCT_SLUG_DUPLICATE
Price Must be >= 0 or null قیمت باید مثبت باشد PRODUCT_INVALID_PRICE
DiscountPrice Must be < Price if set قیمت تخفیف باید کمتر از قیمت اصلی باشد PRODUCT_INVALID_DISCOUNT
StockQuantity Must be >= 0 موجودی نمی‌تواند منفی باشد PRODUCT_NEGATIVE_STOCK
Weight Must be > 0 if set وزن باید مثبت باشد PRODUCT_INVALID_WEIGHT

Order Validation Rules

Field/Rule Validation Rule Error Message (Persian) Error Code
OrderNumber Required, unique, auto-generated شماره سفارش معتبر نیست ORDER_INVALID_NUMBER
Items Must have at least 1 item سفارش باید حداقل یک محصول داشته باشد ORDER_EMPTY
Total Must match calculated total مجموع سفارش صحیح نیست ORDER_TOTAL_MISMATCH
CustomerEmail Required, valid email format ایمیل معتبر نیست ORDER_INVALID_EMAIL
CustomerPhone Required, 11 digits starting with 09 شماره موبایل معتبر نیست ORDER_INVALID_PHONE
ShippingAddress Required, max 500 characters آدرس تحویل الزامی است ORDER_ADDRESS_REQUIRED
Status Transition Must follow valid state machine تغییر وضعیت سفارش مجاز نیست ORDER_INVALID_STATUS_CHANGE

Cart Validation Rules

Rule Validation Error Message (Persian) Error Code
Item Quantity Must be > 0 and <= product stock تعداد درخواستی موجود نیست CART_INSUFFICIENT_STOCK
Maximum Items Cart cannot have > 50 unique items حداکثر 50 محصول مختلف مجاز است CART_MAX_ITEMS_EXCEEDED
Item Total Quantity Total quantity cannot exceed 999 تعداد کل محصولات بیش از حد مجاز است CART_MAX_QUANTITY_EXCEEDED
Product Availability Product must be active این محصول دیگر موجود نیست CART_PRODUCT_UNAVAILABLE
Discount Code Must be active and within date range کد تخفیف معتبر نیست CART_INVALID_DISCOUNT

Domain Events

Event-Driven Architecture

Domain events are raised by aggregate roots when important business events occur. Other parts of the system can listen to these events and react accordingly.

Domain Event Raised When Event Handlers
OrderCreatedEvent New order is placed successfully - Send order confirmation email
- Create notification
- Update analytics
- Log audit trail
OrderStatusChangedEvent Order status is updated - Send status update email
- Create notification
- Update inventory if cancelled
- Trigger shipment if processing
PaymentCompletedEvent Payment is successfully processed - Update order status
- Send payment receipt
- Create notification
- Log payment audit
ProductStockChangedEvent Product stock quantity changes - Check low stock threshold
- Send low stock notification (admin)
- Update search index
- Invalidate cache
ProductReviewApprovedEvent Product review is approved - Update product rating
- Send notification to reviewer
- Invalidate product cache
- Award loyalty points
ShipmentCreatedEvent Shipment is created for order - Send shipping notification email
- Create tracking notification
- Update order status
- Log shipment details
// Example Domain Event
public class OrderCreatedEvent : IDistributedEvent
{
    public Guid OrderId { get; set; }
    public string OrderNumber { get; set; }
    public Guid? UserId { get; set; }
    public decimal Total { get; set; }
    public DateTime CreatedAt { get; set; }
}

// Example Event Handler
public class OrderCreatedEventHandler : IDistributedEventHandler<OrderCreatedEvent>
{
    private readonly IEmailService _emailService;
    private readonly INotificationService _notificationService;

    public async Task HandleEventAsync(OrderCreatedEvent eventData)
    {
        // Send confirmation email
        await _emailService.SendOrderConfirmationAsync(eventData.OrderId);

        // Create in-app notification
        await _notificationService.CreateNotificationAsync(
            eventData.UserId,
            NotificationType.OrderCreated,
            $"سفارش {eventData.OrderNumber} با موفقیت ثبت شد"
        );
    }
}

Domain Services

When to Use Domain Services

Domain services contain business logic that doesn't naturally fit within a single entity or involves multiple aggregates.

Domain Service Responsibility Key Methods
OrderDomainService Complex order operations involving multiple entities CreateOrderFromCart(cart, address)
CancelOrderAndRestoreStock(order)
ProcessRefund(order, amount)
PricingDomainService Calculate prices with complex business rules CalculateProductPrice(product, user)
ApplyVolumeDiscounts(items)
CalculateShippingCost(cart, address)
InventoryDomainService Stock management across products and variants ReserveStock(productId, quantity)
ReleaseStock(productId, quantity)
TransferStock(fromId, toId, quantity)
DiscountDomainService Complex discount calculation logic CalculateApplicableDiscounts(cart)
ValidateDiscountRules(code, cart)
StackDiscounts(discounts)
// Example Domain Service
public class OrderDomainService : DomainService
{
    private readonly IRepository<Product, Guid> _productRepository;
    private readonly IRepository<OrderStatusHistory, Guid> _historyRepository;

    public async Task<Order> CreateOrderFromCartAsync(Cart cart, UserAddress address)
    {
        // Validate cart
        if (cart.IsEmpty())
            throw new BusinessException("سبد خرید خالی است");

        // Check stock availability for all items
        foreach (var item in cart.Items)
        {
            var product = await _productRepository.GetAsync(item.ProductId);
            if (!product.IsInStock() || product.StockQuantity < item.Quantity)
            {
                throw new BusinessException($"محصول {product.Name} موجود نیست");
            }
        }

        // Create order
        var order = new Order(GuidGenerator.Create(), await GenerateOrderNumberAsync());
        order.UserId = cart.UserId;
        order.CustomerName = address.RecipientName;
        order.CustomerPhone = address.RecipientPhone;
        order.ShippingAddress = address.GetFullAddress();
        order.Status = OrderStatus.Pending;

        // Add items and decrease stock
        foreach (var cartItem in cart.Items)
        {
            var orderItem = new OrderItem(GuidGenerator.Create());
            orderItem.ProductId = cartItem.ProductId;
            orderItem.ProductName = cartItem.ProductName;
            orderItem.Quantity = cartItem.Quantity;
            orderItem.UnitPrice = cartItem.Price;
            orderItem.LineTotal = cartItem.Quantity * cartItem.Price;

            order.Items.Add(orderItem);

            // Decrease stock
            var product = await _productRepository.GetAsync(cartItem.ProductId);
            product.DecreaseStock(cartItem.Quantity);
        }

        order.CalculateTotals();

        return order;
    }

    public async Task CancelOrderAndRestoreStockAsync(Order order)
    {
        if (!order.CanBeCancelled())
            throw new BusinessException("این سفارش قابل لغو نیست");

        // Restore product stock
        foreach (var item in order.Items)
        {
            var product = await _productRepository.GetAsync(item.ProductId);
            product.IncreaseStock(item.Quantity);
        }

        // Update order status
        order.UpdateStatus(OrderStatus.Cancelled, null, "لغو شده توسط سیستم");
    }
}

Related Documentation