Skip to content

Instantly share code, notes, and snippets.

@stemmlerjs
Last active March 24, 2021 21:50
Show Gist options
  • Save stemmlerjs/00de3f187458d9d39ef7d63c1650d624 to your computer and use it in GitHub Desktop.
Save stemmlerjs/00de3f187458d9d39ef7d63c1650d624 to your computer and use it in GitHub Desktop.
An example of the trade offs using either aggregates or using domain services to encapsulate business logic. Based on code from https://khalilstemmler.com/articles/typescript-domain-driven-design/ddd-vs-crud-design/
/*
* By designing the Customer as an Aggregate Root and including a reference all of the
* movies that it rented, we can place the validation logic for renting movies
* directly on the customer entity.
*
* Advantages: More declarative-reading code. The operation is a lot closer to the
* entity itself, which improves the discoverability and understanding what a customer
* can do.
*
* Disadvantages: Additional overhead. Having to pull the ids of rented movie everytime we
* retrieve a customer might become cumbersome + slow down performance depending on the use cases.
*/
interface ICustomerProps {
name: string;
rentedMovies: MovieId[]; // one way reference to movie ids
}
class Customer extends AggregateRoot<ICustomerProps> {
private constructor (props: ICustomerProps, id?: UniqueEntityId) {
super(props, id)
}
public static create (props: ICustomerProps, id?: UniqueEntityId): Result<Customer> {
// make all invariants are satisfied
}
public rentMovie (movie: Movie): Result<Customer> {
// if this.rentedMovies is larger than the max amount of movies, then
// this operation should fail
}
}
/*
* Another design: using Domain Services / Use Cases
*
* Advantages: Probably a better design if performance becomes a problem. Domain Services are a good
* place to put domain logic that doesn't belong to one particular entity.
*
* Disadvantages:
*/
class RentMovieUseCase {
private rentalRepo: IrentalRepo;
constructor (rentalRepo: IrentalRepo) {
this.rentalRepo = rentalRepo;
}
public async exec (customer: Customer, movie: Movie): Result<Rental> {
const { movieRepo } = this;
const rentedMovies: Rental[] = await rentalRepo.getRentalsByCustomerId(customer.id);
if (rentedMovies.length > MAX_NUM_MOVIES_TO_RENT) {
return Result.fail<Rental>('Customer rented the max amount of movies')
} else {
return Rental.create({ customerId: customer.id, movieId: movie.id });
}
}
}
// Using the domain service, the controller might look more like
class MovieController extends BaseController {
private movieRepo: IMovieRepo;
private customerRepo: ICustomerRepo;
private rentalRepo: IRentalRepo;
constructor (movieRepo: IMovieRepo, customerRepo: ICustomerRepo, rentalRepo: IRentalRepo) {
super();
this.movieRepo = movieRepo;
this.customerRepo = customerRepo;
this.rentalRepo = rentalRepo;
}
public async rentMovie () {
const { req, movieRepo, customerRepo, rentalRepo } = this;
const { movieId } = req.params['movie'];
const { customerId } = req.params['customer'];
const movie: Movie = await movieRepo.findById(movieId);
const customer: Customer = await customerRepo.findById(customerId);
if (!!movie === false) {
return this.fail('Movie not found')
}
if (!!customer === false) {
return this.fail('Customer not found')
}
const rentMovieResult: Result<Rental> = new RentMovieUseCase(this.rentalRepo).exec(customer, movie)
if (rentMovieResult.isFailure) {
return this.fail(rentMovieResult.error)
} else {
const rental = rentMovieResult.getValue();
await rentalRepo.save(rental);
return this.ok();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment