A production-ready apartment booking platform built on Clean Architecture, DDD, and CQRS — designed for maintainability, testability, and scalability.
- Overview
- Architecture
- Layer Breakdown
- Tech Stack
- Domain Model
- Getting Started
- Project Structure
- Testing
- Design Patterns
Bookify is a full-featured apartment booking system built with C# and .NET, following Clean Architecture principles to ensure a modular, maintainable, and testable codebase.
The system allows users to search for apartments, make reservations, process payments, and leave reviews — all powered by a robust domain model with rich business rules.
Users ──► Search Apartments ──► Reserve ──► Confirm Booking ──► Review
Core Goals:
- Clear separation of concerns across all architectural layers
- Domain-centric design with rich business logic
- Testable by design — every layer is independently verifiable
- Production-ready patterns: Outbox, CQRS, Circuit Breaker, Optimistic Concurrency
Bookify follows a Domain-Centric Clean Architecture, where dependencies always flow inward — outer layers depend on inner layers, never the reverse.
┌─────────────────────────────────────────────┐
│ Presentation Layer │ ← REST API, Controllers, Middleware
├─────────────────────────────────────────────┤
│ Application Layer │ ← Use Cases, CQRS, MediatR, Validation
├─────────────────────────────────────────────┤
│ Domain Layer │ ← Entities, Value Objects, Domain Events
├─────────────────────────────────────────────┤
│ Infrastructure Layer │ ← EF Core, RabbitMQ, Identity, Email
└─────────────────────────────────────────────┘
Key Architectural Principles:
| Principle | Description |
|---|---|
| Separation of Concerns | Each layer has a single, well-defined responsibility |
| Dependency Inversion | Inner layers define interfaces; outer layers implement them |
| Persistence Ignorance | Domain has zero knowledge of how data is stored |
| Bounded Contexts | Clear boundaries prevent domain logic leakage |
| Encapsulation | State changes only happen through well-defined domain methods |
| Single Responsibility | Every class has one reason to change |
The core of the application. Contains pure business logic with no external dependencies.
// Example: Rich Domain Entity
public sealed class Booking : Entity
{
public ApartmentId ApartmentId { get; private set; }
public UserId UserId { get; private set; }
public DateRange Duration { get; private set; }
public Money TotalPrice { get; private set; }
public BookingStatus Status { get; private set; }
public static Result<Booking> Reserve(
Apartment apartment,
UserId userId,
DateRange duration,
PricingService pricingService)
{
// Rich business rules enforced here
}
}Contains: Entities · Value Objects · Domain Events · Domain Services · Repository Interfaces · Exceptions · Enums
Coordinates domain objects to fulfill use cases. Implements CQRS via MediatR.
// Command: Reserve an Apartment
public record ReserveApartmentCommand(
Guid ApartmentId,
Guid UserId,
DateOnly StartDate,
DateOnly EndDate) : ICommand<Guid>;
// Handler
internal sealed class ReserveApartmentCommandHandler : ICommandHandler<ReserveApartmentCommand, Guid>
{
public async Task<Result<Guid>> Handle(
ReserveApartmentCommand request,
CancellationToken cancellationToken)
{
// Orchestrate: fetch → validate → execute → persist → publish
}
}Contains: Commands & Queries · Handlers · Validators (FluentValidation) · Behaviors (Pipeline) · DTOs · Abstractions
Implements the interfaces defined by inner layers. Handles all I/O concerns.
// EF Core Repository Implementation
internal sealed class ApartmentRepository : IApartmentRepository
{
private readonly ApplicationDbContext _context;
public async Task<Apartment?> GetByIdAsync(
ApartmentId id,
CancellationToken cancellationToken = default) =>
await _context.Apartments
.FirstOrDefaultAsync(a => a.Id == id, cancellationToken);
}Contains: EF Core DbContext · Entity Configurations · Repositories · Keycloak Integration · RabbitMQ Publisher · Email Provider · Outbox Pattern · Background Jobs
Exposes the application via a REST API. Thin layer — delegates everything to Application.
[ApiController]
[Route("api/v{version:apiVersion}/bookings")]
public class BookingsController : ControllerBase
{
[HttpPost]
public async Task<IActionResult> ReserveApartment(
ReserveApartmentRequest request,
CancellationToken cancellationToken)
{
var command = new ReserveApartmentCommand(/* map request */);
var result = await _sender.Send(command, cancellationToken);
return result.IsSuccess ? Ok(result.Value) : BadRequest(result.Error);
}
}Contains: Controllers / Minimal API Endpoints · Middleware · DI Composition Root · Docker Compose Setup · API Versioning
The system is centered around four core aggregates:
┌──────────────┐ ┌─────────────────────────────────────┐
│ User │ │ Booking │
│──────────────│ ┌───►│──────────────────────────────────── │
│ Id: Guid │ │ │ Id · ApartmentId · UserId │
│ FirstName │────┘ │ Start · End · PriceForPeriod │
│ LastName │ │ CleaningFee · AmenitiesUpCharge │
│ Email │ │ TotalPrice · Status │
└──────────────┘ │ CreatedOnUtc · ConfirmedOnUtc │
└───────────────────┬─────────────────┘
┌──────────────────────┐ │
│ Apartment │ │
│──────────────────────│ ▼
│ Id · Name │ ┌───────────────────┐
│ Description │ │ Review │
│ Address (VO) │ │───────────────────│
│ Price (Money VO) │ │ Id · ApartmentId │
│ CleaningFee │ │ BookingId · UserId│
│ Amenities[] │ │ Rating · Comment │
│ LastBookedOnUtc │ └───────────────────┘
└──────────────────────┘
Value Objects: Money · Address · DateRange · ApartmentId · UserId · BookingId
- .NET 8 SDK
- Docker & Docker Compose
- SQL Server (or use Docker)
# Clone the repository
git clone https://github.com/Alnuimi/Bookify.git
cd Bookify
# Start all services (SQL Server, RabbitMQ, Redis, Keycloak)
docker-compose up -d
# Apply database migrations
dotnet ef database update --project src/Bookify.Infrastructure
# Run the API
dotnet run --project src/Bookify.Api# Restore dependencies
dotnet restore
# Build the solution
dotnet build
# Run tests
dotnet test
# Start the API
dotnet run --project src/Bookify.ApiThe API will be available at https://localhost:5001 with Swagger at https://localhost:5001/swagger.
Bookify/
│
├── src/
│ ├── Bookify.Domain/ # 🟣 Core business logic
│ │ ├── Apartments/
│ │ │ ├── Apartment.cs # Aggregate root
│ │ │ ├── Address.cs # Value object
│ │ │ └── Money.cs # Value object
│ │ ├── Bookings/
│ │ │ ├── Booking.cs # Aggregate root
│ │ │ ├── BookingStatus.cs # Enum
│ │ │ └── Events/ # Domain events
│ │ └── Users/
│ │ └── User.cs
│ │
│ ├── Bookify.Application/ # 🔴 Use cases & orchestration
│ │ ├── Abstractions/
│ │ │ ├── ICommand.cs
│ │ │ └── IQuery.cs
│ │ ├── Apartments/
│ │ │ └── SearchApartments/ # Query + Handler
│ │ ├── Bookings/
│ │ │ ├── ReserveApartment/ # Command + Handler
│ │ │ └── ConfirmBooking/ # Command + Handler
│ │ └── Behaviors/ # Pipeline behaviors
│ │ ├── LoggingBehavior.cs
│ │ └── ValidationBehavior.cs
│ │
│ ├── Bookify.Infrastructure/ # 🔵 External integrations
│ │ ├── Data/
│ │ │ ├── ApplicationDbContext.cs
│ │ │ └── Configurations/ # EF entity configs
│ │ ├── Repositories/
│ │ ├── Authentication/ # Keycloak integration
│ │ ├── Messaging/ # RabbitMQ
│ │ ├── Outbox/ # Outbox pattern
│ │ └── BackgroundJobs/
│ │
│ └── Bookify.Api/ # 🟢 REST API entry point
│ ├── Controllers/
│ ├── Middleware/
│ └── Program.cs
│
└── tests/
├── Bookify.Domain.UnitTests/
├── Bookify.Application.UnitTests/
├── Bookify.Application.IntegrationTests/
└── Bookify.Architecture.Tests/
The solution includes a comprehensive testing strategy across all layers:
# Run all tests
dotnet test
# Run with coverage
dotnet test --collect:"XPlat Code Coverage"| Test Type | Target | Tool |
|---|---|---|
| Unit Tests | Domain logic, Application handlers | xUnit + NSubstitute |
| Integration Tests | Infrastructure, API endpoints | WebApplicationFactory |
| Architecture Tests | Layer dependency rules | NetArchTest |
Architecture tests enforce that:
- Domain has zero external dependencies
- Application only references Domain
- Infrastructure does not reference Presentation
- Controllers are thin and delegate to MediatR
| Pattern | Location | Purpose |
|---|---|---|
| CQRS | Application | Separate read/write models |
| Repository | Domain/Infrastructure | Persistence abstraction |
| Outbox | Infrastructure | Reliable domain event publishing |
| Unit of Work | Infrastructure | Transactional consistency |
| Pipeline Behavior | Application | Cross-cutting concerns (logging, validation) |
| Result Pattern | Domain/Application | Explicit error handling without exceptions |
| Optimistic Concurrency | Infrastructure | Conflict detection in concurrent updates |
| Domain Events | Domain | Decouple side effects from core logic |