Skip to main content

Overview

The Domain layer represents the core business logic and entities of PriceSignal. It contains domain models, value objects, and domain events following Domain-Driven Design (DDD) principles. This layer has no dependencies on other layers and defines the business rules.

Base Entities

EventEntity

The foundation for domain events, allowing entities to raise and manage events.
public abstract class EventEntity 
{
    private readonly List<BaseEvent> _events = new();
    
    [NotMapped]
    public IReadOnlyCollection<BaseEvent> Events => _events.AsReadOnly();
    
    public void AddEvent(BaseEvent @event) { /* ... */ }
    public void RemoveEvent(BaseEvent @event) { /* ... */ }
    public void ClearEvents() { /* ... */ }
}

BaseEntity

Provides identity tracking for all domain entities.
public abstract class BaseEntity : EventEntity
{
    public long Id { get; set; }
    public Guid EntityId { get; set; }
}
src/Domain/Models/BaseEntity.cs:5

BaseAuditableEntity

Extends BaseEntity with audit timestamps for tracking creation, modification, and soft deletion.
public abstract class BaseAuditableEntity : BaseEntity
{
    public DateTime CreatedAt { get; set; }
    public DateTime? ModifiedAt { get; set; }
    public DateTime? DeletedAt { get; set; }
}
src/Domain/Models/BaseAuditableEntity.cs:3

Core Domain Models

Exchange

Represents a cryptocurrency exchange (e.g., Binance, Coinbase).
public class Exchange : BaseAuditableEntity
{
    public required string Name { get; init; }
    public string? Description { get; init; }
    public IList<Instrument> Instruments { get; init; } = new List<Instrument>();
}
src/Domain/Models/Exchanges/Exchange.cs:5

Instrument

Represents a tradable cryptocurrency pair (e.g., BTC/USDT).
public class Instrument : BaseAuditableEntity
{
    public required string Symbol { get; init; }
    public required string Name { get; init; }
    public string? Description { get; init; }
    public required string BaseAsset { get; init; } 
    public required string QuoteAsset { get; init; }
    public Exchange Exchange { get; private set; } = default!;
    
    public void SetExchange(Exchange exchange)
    {
        Exchange = exchange;
    }
}
src/Domain/Models/Instruments/Instrument.cs:5

InstrumentPrice

Captures real-time price data for an instrument.
public class InstrumentPrice : EventEntity
{
    public required string Symbol { get; init; }
    public required decimal Price { get; init; }
    public decimal Volume { get; init; }
    public decimal Quantity { get; init; }
    public DateTimeOffset Timestamp { get; init; }
    public Exchange Exchange { get; private set; } = default!;
    
    public void SetExchange(Exchange exchange)
    {
        Exchange = exchange;
    }
}
src/Domain/Models/Instruments/InstrumentPrice.cs:5

User

Represents a PriceSignal user with their subscriptions and notification preferences.
public class User
{
    public string Id { get; set; }
    public string Email { get; set; }
    public string? StripeCustomerId { get; set; }
    public ICollection<Subscription> Subscriptions { get; set; } = new List<Subscription>();
    public ICollection<PriceRule> PriceRules { get; set; } = new List<PriceRule>();
    public ICollection<UserNotificationChannel> NotificationChannels { get; set; } = new List<UserNotificationChannel>();
}
src/Domain/Models/User/User.cs:3

Price Rules Aggregate

PriceRule

The core aggregate root for user-defined price alerts and conditions.
public class PriceRule : BaseAuditableEntity
{
    public required string Name { get; set; }
    public required string Description { get; set; }
    public bool IsEnabled { get; set; }
    public DateTime? LastTriggeredAt { get; set; }
    public decimal? LastTriggeredPrice { get; set; }
    public NotificationChannelType NotificationChannel { get; set; }
    public Instrument Instrument { get; set; }
    public required long InstrumentId { get; set; }
    public ICollection<PriceCondition> Conditions { get; set; } = new List<PriceCondition>();
    public User? User { get; set; }
    public string? UserId { get; init; }
    public ICollection<PriceRuleTriggerLog> ActivationLogs { get; set; } = new List<PriceRuleTriggerLog>();
    
    [NotMapped]
    public bool HasAttempted { get; set; } = false;
    
    public void Trigger(decimal price)
    {
        LastTriggeredPrice = price;
        LastTriggeredAt = DateTime.UtcNow;
        if (Events.Count != 0) return;
        AddEvent(new PriceRuleTriggeredEvent(this));
        ActivationLogs.Add(new PriceRuleTriggerLog(this));
    }
}
src/Domain/Models/PriceRule/PriceRule.cs:8 Key features:
  • Supports multiple conditions that must all be met
  • Tracks trigger history with snapshots
  • Raises domain events when triggered
  • Supports cooldown periods to prevent spam

PriceCondition

Defines specific conditions for price rule evaluation.
public class PriceCondition : BaseAuditableEntity
{
    public required string ConditionType { get; set; }
    public decimal Value { get; set; }
    public JsonDocument AdditionalValues { get; set; }
    [JsonIgnore]
    public PriceRule Rule { get; set; }
}

public enum ConditionType
{
    PricePercentage,
    Price,
    PriceAction,
    PriceCrossover,
    TechnicalIndicator,
}
src/Domain/Models/PriceRule/PriceCondition.cs:6 Condition types:
  • Price: Trigger when price crosses a threshold
  • PricePercentage: Trigger on percentage change
  • PriceCrossover: Trigger when price crosses above/below a level
  • TechnicalIndicator: Trigger based on RSI, SMA, EMA

PriceRuleTriggerLog

Immutable audit log of price rule activations.
public class PriceRuleTriggerLog : BaseAuditableEntity
{
    public decimal? Price { get; set; }
    public decimal? PriceChange { get; set; }
    public decimal? PriceChangePercentage { get; set; }
    public DateTime TriggeredAt { get; init; }
    public PriceRule PriceRule { get; init; }
    public JsonDocument PriceRuleSnapshot { get; init; }
}
src/Domain/Models/PriceRule/PriceRuleHistory.cs:7

Domain Events

BaseEvent

Base class for all domain events, integrated with MediatR.
public abstract class BaseEvent : INotification
{
}
src/Domain/Models/BaseEvent.cs:5

PriceRuleTriggeredEvent

Raised when a price rule’s conditions are met.
public class PriceRuleTriggeredEvent(PriceRule rule) : BaseEvent
{
    public PriceRule Rule { get; set; } = rule;
}
src/Domain/Models/PriceRule/Events/PriceRuleTriggeredEvent.cs:3 This event triggers:
  • Notification delivery to users
  • Trigger log creation
  • Analytics tracking

Design Patterns

Aggregate Pattern

PriceRule acts as an aggregate root, ensuring consistency:
  • Controls access to PriceCondition entities
  • Manages its own state transitions
  • Raises domain events at aggregate boundaries

Rich Domain Model

Entities contain business logic, not just data:
  • PriceRule.Trigger() encapsulates triggering logic
  • Instrument.SetExchange() maintains invariants

Event Sourcing (Partial)

While not full event sourcing, the system:
  • Captures all state changes as events
  • Maintains immutable trigger logs
  • Uses snapshots for historical queries

Validation Rules

  • Instruments must have valid base and quote assets
  • Price rules require at least one condition
  • Soft deletes prevent data loss (DeletedAt pattern)
  • EntityId (GUID) used for external references, Id (long) for internal performance

Next Steps

Build docs developers (and LLMs) love