Skip to content

14: Domain Design & Business Logic

This section focuses on designing the Domain Layer and Business Logic Layer for Search.Api and Checkin.Api. While Section 13 defined the overall Clean Architecture shape, this section goes deeper into the actual domain modeling, rules, validation, domain events, and how the APIs enforce business processes.

If Section 13 explained the structure, Section 14 explains the behaviour.

14.1 What Is the Domain Layer?

The Domain Layer contains:

• Entities
• Value Objects
• Aggregates
• Domain Services
• Domain Events
• Validation Rules
• Business invariants

It is pure logic:

✓ No EF Core
✓ No Redis
✓ No RabbitMQ
✓ No logging
✓ No external APIs

This layer represents the heart of your system: what your system is, not how it stores or exposes data.

14.2 Domain Design Principles

14.2.1 The Domain Is the Most Stable Layer

Business rules change slowly compared to infrastructure. Your domain model should outlive:

• Frameworks
• Databases
• UI layers
• Messaging systems

If PostgreSQL disappears tomorrow and you switch to MongoDB: Your Domain Layer should remain untouched.

14.2.2 Entities, Value Objects, Aggregates

Entities: Objects with identity (ID) that changes over time.

Examples: SearchRequest, SearchResult, CheckinRecord, Passenger

Value Objects: Immutable objects defined by value, not identity.

Examples: DateRange, Price, EmailAddress, Coordinates

Aggregates: Clusters of related entities with one entry point (Aggregate Root).

Examples: Checkin (root) → Passenger, TicketNumber, TimeWindow SearchResult (root) → Individual offers, metadata

Aggregates enforce rules like:

• No invalid checkin times
• All passengers must have valid documents
• Search results must have consistent pricing

14.2.3 Domain Services

Used only when unavoidable:

• Logic does NOT belong to a specific entity
• Logic spans multiple aggregates
• Complex validations involving multiple inputs

Example: Checkin rules involving airline restrictions + passenger age.

Domain services are pure logic, no infrastructure calls.

14.2.4 Domain Events

Events that represent something important happening:

CheckinCompleted
SearchPerformed
PassengerAdded
PriceChanged

Events can trigger:

✓ Cache invalidation
✓ RabbitMQ publishes (via Application Layer)
✓ Internal logic in the domain

Domain events live in the Domain Layer, but the handlers execute in the Application Layer.

14.3 Domain Models for Search.Api

14.3.1 Search Criteria (Value Object)

Example:

public record SearchCriteria(string Origin, string Destination, DateOnly Date);

Rules:

• Origin cannot equal Destination
• Date must be in the future
• Strings must be valid IATA codes

14.3.2 SearchResult (Aggregate Root)

public class SearchResult {
    public Guid Id { get; private set; }
    public DateTime CreatedAt { get; private set; }
    public IReadOnlyList<FlightOffer> Offers => _offers;
}

Rules:

• All offers must match the original criteria
• Prices must be non-negative
• Results expire after a short window (for caching)

14.3.3 FlightOffer (Entity)

public class FlightOffer {
    public decimal Price { get; private set; }
    public string FareType { get; private set; }
    public IEnumerable<Segment> Segments { get; private set; }
}

Rules:

• Price >= 0
• Segments must be contiguous in time
• No overlapping segments

14.4 Domain Models for Checkin.Api

14.4.1 Passenger (Entity)

public class Passenger {
    public string FirstName { get; }
    public string LastName { get; }
    public DateOnly BirthDate { get; }
}

Rules:

• FirstName/LastName cannot be empty
• Passenger must have a valid age for the flight (Application Layer might check airline rules)

14.4.2 TicketNumber (Value Object)

public record TicketNumber(string Value) {
    public TicketNumber {
        if (!Regex.IsMatch(Value, "^[A-Z0-9]+$"))
            throw new DomainException("Invalid ticket number format.");
    }
}

14.4.3 CheckinRecord (Aggregate Root)

public class CheckinRecord {
    public Guid Id { get; }
    public Passenger Passenger { get; }
    public TicketNumber TicketNumber { get; }
    public DateTime CheckinTime { get; private set; }
    public bool Completed { get; private set; }
}

Rules:

✔ Check-in cannot occur twice
✔ Time must be within check-in window
✔ Must generate domain event CheckinCompleted

14.5 Business Workflow (Application Layer)

The Application Layer coordinates the domain model.

Example: Search Workflow

1. Validate search criteria (Application Layer + Domain Rules)
2. Check Redis cache
3. Query PostgreSQL (fallback to remote integrators if needed)
4. Map results to domain model
5. Publish SearchPerformed domain event
6. Save search result if needed (PostgreSQL)
7. Return DTO to API

Example: Checkin Workflow

1. Receive check-in request DTO
2. Validate passenger details
3. Load flight rules (if needed)
4. Create CheckinRecord aggregate
5. Add CheckinCompleted domain event
6. Save to PostgreSQL
7. Publish RabbitMQ message
8. Return confirmation

14.6 Domain Validation Strategy

Validation belongs in:

✓ Domain Layer → Business rules, invariants
✓ Application Layer → Use-case logic, coordination
✗ Controllers → Should not contain logic

Use:

• Exceptions for domain rule violations
• FluentValidation in Application Layer for request DTOs

Example:

if (criteria.Date < DateOnly.FromDateTime(DateTime.UtcNow))
    throw new DomainException("Search date must be in the future.");

Domain exceptions bubble to the Application Layer and return:

400 Bad Request

14.7 Domain Events → Outbox Pattern (Optional Future)

If you want guaranteed event delivery, you can add an Outbox Table:

• Save event in PostgreSQL transaction
• Background worker reads outbox
• Publishes to RabbitMQ
• Marks outbox row as processed

This prevents:

✓ Lost events
✓ Duplicate messages
✓ Inconsistent state between database & queue

14.8 Summary of Section 14

You now have:

✔ Domain Models for Search & Checkin
✔ Clear Entity/Value Object/Aggregate definitions
✔ Domain Events
✔ Application workflow structure
✔ Validation strategy
✔ Domain-first business design

This ensures that LocalCloudLab grows as a real scalable platform, not a pile of controllers glued together.

Next section will continue automatically: Section 15 – Envoy Gateway Advanced Routing & Traffic Management