Sitemap

Domain-Driven Design in .NET: A Practical Guide with Hotel Booking Example

4 min readJun 1, 2025

Learn how to apply Domain-Driven Design (DDD) in .NET with real-world hotel booking examples. Discover key concepts like entities, value objects, aggregates, repositories, and services, and structure your application for better testability and scalability.

Domain-Driven Design (DDD) is a powerful architectural approach to building maintainable, testable, and business-aligned applications, especially when working with complex domains.

In this article, we’ll walk through the core DDD concepts and show how to implement them in a real-world hotel booking context using .NET.

What Is Domain-Driven Design?

DDD approach, introduced by Eric Evans (2004), emphasizes aligning software models closely with business concepts. It helps teams:

  • Focus on core business logic
  • Create rich domain models
  • Separate infrastructure concerns
  • Speak a Ubiquitous Language shared by both developers and domain experts

Suggested Architecture

Here’s how to organize our solution:

/Domain
├── Entities
├── ValueObjects
├── Interfaces
└── DomainServices

/Application
└── UseCases / AppServices

/Infrastructure
├── Repositories
└── ExternalServices

/WebApi
└── Controllers

Ubiquitous Language in Action

With DDD, both developers and business experts use the same terms:
“Hotel”, “Availability”, “Booking”, “Check-in” , these are reflected directly in code.

This makes your code not just technical, but communicative and understandable.

Photo by Elizeu Dias on Unsplash

Core DDD Building Blocks

Let’s explore the key components of DDD using a hotel booking scenario.

1. Entity

An entity has a unique identity and changes over time.

Hotel

public class Hotel
{
public Guid Id { get; }
public string Name { get; }
public Address Address { get; }

private readonly List<Booking> _bookings = new();

public Hotel(Guid id, string name, Address address)
{
Id = id;
Name = name;
Address = address;
}

public bool IsAvailable(DateTime checkIn, DateTime checkOut)
{
return !_bookings.Any(b => (checkIn < b.CheckOut) && (checkOut > b.CheckIn));
}

public Guid Book(DateTime checkIn, DateTime checkOut)
{
if (!IsAvailable(checkIn, checkOut))
throw new InvalidOperationException("Hotel is not available for the selected dates.");

var booking = new Booking(this.Id, checkIn, checkOut);
_bookings.Add(booking);
return booking.Id;
}

public BookingStatus GetBookingStatus(Guid bookingId)
{
var booking = _bookings.FirstOrDefault(b => b.Id == bookingId);
if (booking == null)
return BookingStatus.NotFound;

return booking.Status;
}
}

Booking

public class Booking
{
public Guid Id { get; }
public Guid HotelId { get; set;}
public DateTime CheckIn { get; }
public DateTime CheckOut { get; }
public BookingStatus Status { get; private set; }

public Booking(Guid hotelId, DateTime checkIn, DateTime checkOut)
{
if (checkIn >= checkOut)
throw new ArgumentException("Check-out must be after check-in.");

Id = Guid.NewGuid();
HotelId = hotelId;
CheckIn = checkIn;
CheckOut = checkOut;
Status = BookingStatus.Confirmed;
}
}
public enum BookingStatus
{
Confirmed,
Cancelled,
Completed,
NotFound
}

2. Value Object

A value object has no identity and is compared by its properties.

HotelLocation

public class HotelLocation
{
public string City { get; }
public string Country { get; }
public string Region { get; }
public double Latitude { get; }
public double Longitude { get; }

public HotelLocation(string city, string country, string region, double latitude, double longitude)
{
City = city;
Country = country;
Region = region;
Latitude = latitude;
Longitude = longitude;
}

public override bool Equals(object? obj) =>
obj is HotelLocation other &&
City == other.City &&
Country == other.Country &&
Region == other.Region &&
Latitude == other.Latitude &&
Longitude == other.Longitude;

public override int GetHashCode() =>
HashCode.Combine(City, Country, Region, Latitude, Longitude);
}

3. Aggregate Root

Aggregates group related entities and ensure consistency boundaries. Access and modify data only through the aggregate root.

HotelSearch

public class HotelSearch
{
private readonly List<Hotel> _hotels = new();

public IReadOnlyCollection<Hotel> Results => _hotels;

public void AddHotel(Hotel hotel)
{
if (_hotels.Any(h => h.Id == hotel.Id)) return;
_hotels.Add(hotel);
}

public Hotel? FindAvailableHotel(Guid hotelId, DateTime checkIn, DateTime checkOut)
{
var hotel = _hotels.FirstOrDefault(h => h.Id == hotelId); // .. search logic
return hotel?.IsAvailable(checkIn, checkOut) == true ? hotel : null;
}
}

4. Repository Interface

Repositories abstract data access and operate only on aggregate roots.

public interface IHotelRepository
{
Hotel? GetById(Guid id);
void Save(Hotel hotel);
}

5. Domain Service

Use domain services when logic spans across multiple entities and doesn’t naturally fit within one.

HotelBookingService

public class HotelBookingService
{
private readonly IHotelRepository _repository;

public HotelBookingService(IHotelRepository repository)
{
_repository = repository;
}

public Guid BookHotel(Guid hotelId, DateTime checkIn, DateTime checkOut)
{
var hotel = _repository.GetById(hotelId);
if (hotel == null)
throw new InvalidOperationException("Hotel not found.");

var bookingId = hotel.Book(checkIn, checkOut);
_repository.Save(hotel);

return bookingId;
}

public BookingStatus GetBookingStatus(Guid hotelId, Guid bookingId)
{
var hotel = _repository.GetById(hotelId);
return hotel?.GetBookingStatus(bookingId) ?? BookingStatus.NotFound;
}
}

6. Application Service

Coordinates domain operations, serving as an entry point for external interfaces (like APIs).

HotelAppService

public class HotelAppService
{
private readonly HotelBookingService _bookingService;
private readonly HotelSearch _hotelSearch;

public HotelAppService(HotelBookingService bookingService, HotelSearch hotelSearch)
{
_bookingService = bookingService;
_hotelSearch = hotelSearch;
}

public Guid MakeBooking(Guid hotelId, DateTime checkIn, DateTime checkOut)
{
return _bookingService.BookHotel(hotelId, checkIn, checkOut);
}

public BookingStatus QueryBookingStatus(Guid hotelId, Guid bookingId)
{
return _bookingService.GetBookingStatus(hotelId, bookingId);
}

public IReadOnlyCollection<Hotel> SearchAvailability(DateTime checkIn, DateTime checkOut)
{
return _hotelSearch
.Results
.Where(h => h.IsAvailable(checkIn, checkOut))
.ToList();
}
}

Unit Test Example

With proper boundaries and clear responsibilities, testing becomes painless.

[Fact]
public void Hotel_Should_Return_Correct_Booking_Status()
{
var location = new HotelLocation("Antalya", "Turkiye", "Belek", 37.6050, 39.9633);
var hotel = new Hotel(Guid.NewGuid(), "SandBeachX Resort", location);

var bookingId = hotel.Book(DateTime.Today.AddDays(1), DateTime.Today.AddDays(3));

var status = hotel.GetBookingStatus(bookingId);

Assert.Equal(BookingStatus.Confirmed, status);
}

We’ve modeled a hotel booking domain using Domain-Driven Design principles in .NET.

Key takeaways:

  • Use Value Objects (like HotelLocation) for rich, immutable data.
  • Keep Entities like Hotel and Booking focused on business rules.
  • Delegate workflows like availability and booking to Application Services.
  • Use Domain Services for coordinating behavior that spans multiple entities.

This layered approach helps build systems that are expressive, testable, and easy to evolve.

--

--

Mucahid Uslu
Mucahid Uslu

Written by Mucahid Uslu

Physics (Istanbul Technical University) , Software Engineer

Responses (4)