Domain Driven Design, The Repository pattern
I’ve seen so many times a heated discussion about Repositories in Domain Driven Design, there is a kind of misunderstanding and/or confusion about what really a Repository is.
People usually disagree about the role of the Repository, if it should be a kind of “Data Access Layer”, such as a DAO or if it must be something that is injected in the domain class, then it will talk to the DAO in the beneath layer.
I’ve always wondered why people had these fights, but now I can see why ! The thing is, as far as I read in the book (Chapter 10 : Supple Design), the examples of Reposotory has been shown almost exactly as a DAO implementation. I even downloaded the source code examples from the Domain Driven Design Community, and the example IS a DAO implementation, as I’m showing below :
package se.citerus.dddsample.domain.model.cargo;
import java.util.List;
public interface CargoRepository {
/**
* Finds a cargo using given id.
*
* @param trackingId Id
* @return Cargo if found, else {@code null}
*/
Cargo find(TrackingId trackingId);
/**
* Finds all cargo.
*
* @return All cargo.
*/
List findAll();
/**
* Saves given cargo.
*
* @param cargo cargo to save
*/
void store(Cargo cargo);
/**
* @return A unique, generated tracking Id.
*/
TrackingId nextTrackingId();
}
package se.citerus.dddsample.domain.model.cargo;
import java.util.List;
public interface CargoRepository {
/**
* Finds a cargo using given id.
*
* @param trackingId Id
* @return Cargo if found, else {@code null}
*/
Cargo find(TrackingId trackingId);
/**
* Finds all cargo.
*
* @return All cargo.
*/
List findAll();
/**
* Saves given cargo.
*
* @param cargo cargo to save
*/
void store(Cargo cargo);
/**
* @return A unique, generated tracking Id.
*/
TrackingId nextTrackingId();
}and the implementation
package se.citerus.dddsample.infrastructure.persistence.hibernate;
import org.springframework.stereotype.Repository;
import se.citerus.dddsample.domain.model.cargo.Cargo;
import se.citerus.dddsample.domain.model.cargo.CargoRepository;
import se.citerus.dddsample.domain.model.cargo.TrackingId;
import java.util.List;
import java.util.UUID;
/**
* Hibernate implementation of CargoRepository.
*/
@Repository
public class CargoRepositoryHibernate extends HibernateRepository implements CargoRepository {
public Cargo find(TrackingId tid) {
return (Cargo) getSession().
createQuery("from Cargo where trackingId = :tid").
setParameter("tid", tid).
uniqueResult();
}
public void store(Cargo cargo) {
getSession().saveOrUpdate(cargo);
// Delete-orphan does not seem to work correctly when the parent is a component
getSession().createSQLQuery("delete from Leg where cargo_id = null").executeUpdate();
}
public TrackingId nextTrackingId() {
// TODO use an actual DB sequence here, UUID is for in-mem
final String random = UUID.randomUUID().toString().toUpperCase();
return new TrackingId(
random.substring(0, random.indexOf("-"))
);
}
public List findAll() {
return getSession().createQuery("from Cargo").list();
}
}
package se.citerus.dddsample.infrastructure.persistence.hibernate;
import org.springframework.stereotype.Repository;
import se.citerus.dddsample.domain.model.cargo.Cargo;
import se.citerus.dddsample.domain.model.cargo.CargoRepository;
import se.citerus.dddsample.domain.model.cargo.TrackingId;
import java.util.List;
import java.util.UUID;
/**
* Hibernate implementation of CargoRepository.
*/
@Repository
public class CargoRepositoryHibernate extends HibernateRepository implements CargoRepository {
public Cargo find(TrackingId tid) {
return (Cargo) getSession().
createQuery("from Cargo where trackingId = :tid").
setParameter("tid", tid).
uniqueResult();
}
public void store(Cargo cargo) {
getSession().saveOrUpdate(cargo);
// Delete-orphan does not seem to work correctly when the parent is a component
getSession().createSQLQuery("delete from Leg where cargo_id = null").executeUpdate();
}
public TrackingId nextTrackingId() {
// TODO use an actual DB sequence here, UUID is for in-mem
final String random = UUID.randomUUID().toString().toUpperCase();
return new TrackingId(
random.substring(0, random.indexOf("-"))
);
}
public List findAll() {
return getSession().createQuery("from Cargo").list();
}
}This Repository is used by a class called BookingServiceImpl :
package se.citerus.dddsample.application.impl;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.transaction.annotation.Transactional;
import se.citerus.dddsample.application.BookingService;
import se.citerus.dddsample.domain.model.cargo.*;
import se.citerus.dddsample.domain.model.location.Location;
import se.citerus.dddsample.domain.model.location.LocationRepository;
import se.citerus.dddsample.domain.model.location.UnLocode;
import se.citerus.dddsample.domain.service.RoutingService;
import java.util.Collections;
import java.util.Date;
import java.util.List;
public final class BookingServiceImpl implements BookingService {
private final CargoRepository cargoRepository;
private final LocationRepository locationRepository;
private final RoutingService routingService;
private final Log logger = LogFactory.getLog(getClass());
public BookingServiceImpl(final CargoRepository cargoRepository,
final LocationRepository locationRepository,
final RoutingService routingService) {
this.cargoRepository = cargoRepository;
this.locationRepository = locationRepository;
this.routingService = routingService;
}
@Override
@Transactional
public TrackingId bookNewCargo(final UnLocode originUnLocode,
final UnLocode destinationUnLocode,
final Date arrivalDeadline) {
// TODO modeling this as a cargo factory might be suitable
final TrackingId trackingId = cargoRepository.nextTrackingId();
final Location origin = locationRepository.find(originUnLocode);
final Location destination = locationRepository.find(destinationUnLocode);
final RouteSpecification routeSpecification = new RouteSpecification(origin, destination, arrivalDeadline);
final Cargo cargo = new Cargo(trackingId, routeSpecification);
cargoRepository.store(cargo);
logger.info("Booked new cargo with tracking id " + cargo.trackingId().idString());
return cargo.trackingId();
}
@Override
@Transactional
public List requestPossibleRoutesForCargo(final TrackingId trackingId) {
final Cargo cargo = cargoRepository.find(trackingId);
if (cargo == null) {
return Collections.emptyList();
}
return routingService.fetchRoutesForSpecification(cargo.routeSpecification());
}
@Override
@Transactional
public void assignCargoToRoute(final Itinerary itinerary, final TrackingId trackingId) {
final Cargo cargo = cargoRepository.find(trackingId);
if (cargo == null) {
throw new IllegalArgumentException("Can't assign itinerary to non-existing cargo " + trackingId);
}
cargo.assignToRoute(itinerary);
cargoRepository.store(cargo);
logger.info("Assigned cargo " + trackingId + " to new route");
}
@Override
@Transactional
public void changeDestination(final TrackingId trackingId, final UnLocode unLocode) {
final Cargo cargo = cargoRepository.find(trackingId);
final Location newDestination = locationRepository.find(unLocode);
final RouteSpecification routeSpecification = new RouteSpecification(
cargo.origin(), newDestination, cargo.routeSpecification().arrivalDeadline()
);
cargo.specifyNewRoute(routeSpecification);
cargoRepository.store(cargo);
logger.info("Changed destination for cargo " + trackingId + " to " + routeSpecification.destination());
}
}
package se.citerus.dddsample.application.impl;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.transaction.annotation.Transactional;
import se.citerus.dddsample.application.BookingService;
import se.citerus.dddsample.domain.model.cargo.*;
import se.citerus.dddsample.domain.model.location.Location;
import se.citerus.dddsample.domain.model.location.LocationRepository;
import se.citerus.dddsample.domain.model.location.UnLocode;
import se.citerus.dddsample.domain.service.RoutingService;
import java.util.Collections;
import java.util.Date;
import java.util.List;
public final class BookingServiceImpl implements BookingService {
private final CargoRepository cargoRepository;
private final LocationRepository locationRepository;
private final RoutingService routingService;
private final Log logger = LogFactory.getLog(getClass());
public BookingServiceImpl(final CargoRepository cargoRepository,
final LocationRepository locationRepository,
final RoutingService routingService) {
this.cargoRepository = cargoRepository;
this.locationRepository = locationRepository;
this.routingService = routingService;
}
@Override
@Transactional
public TrackingId bookNewCargo(final UnLocode originUnLocode,
final UnLocode destinationUnLocode,
final Date arrivalDeadline) {
// TODO modeling this as a cargo factory might be suitable
final TrackingId trackingId = cargoRepository.nextTrackingId();
final Location origin = locationRepository.find(originUnLocode);
final Location destination = locationRepository.find(destinationUnLocode);
final RouteSpecification routeSpecification = new RouteSpecification(origin, destination, arrivalDeadline);
final Cargo cargo = new Cargo(trackingId, routeSpecification);
cargoRepository.store(cargo);
logger.info("Booked new cargo with tracking id " + cargo.trackingId().idString());
return cargo.trackingId();
}
@Override
@Transactional
public List requestPossibleRoutesForCargo(final TrackingId trackingId) {
final Cargo cargo = cargoRepository.find(trackingId);
if (cargo == null) {
return Collections.emptyList();
}
return routingService.fetchRoutesForSpecification(cargo.routeSpecification());
}
@Override
@Transactional
public void assignCargoToRoute(final Itinerary itinerary, final TrackingId trackingId) {
final Cargo cargo = cargoRepository.find(trackingId);
if (cargo == null) {
throw new IllegalArgumentException("Can't assign itinerary to non-existing cargo " + trackingId);
}
cargo.assignToRoute(itinerary);
cargoRepository.store(cargo);
logger.info("Assigned cargo " + trackingId + " to new route");
}
@Override
@Transactional
public void changeDestination(final TrackingId trackingId, final UnLocode unLocode) {
final Cargo cargo = cargoRepository.find(trackingId);
final Location newDestination = locationRepository.find(unLocode);
final RouteSpecification routeSpecification = new RouteSpecification(
cargo.origin(), newDestination, cargo.routeSpecification().arrivalDeadline()
);
cargo.specifyNewRoute(routeSpecification);
cargoRepository.store(cargo);
logger.info("Changed destination for cargo " + trackingId + " to " + routeSpecification.destination());
}
}Well, I cannot deny that I did not dig so deep into the code ( and I did not finish the book ) yet,
but what I can state for sure is that this architecture really looks like this sequence :

Also, the layering :

Ok, I know I am over simplifying things here, but my intention is only to communicate my point of view about
the topic. I’m not saying that what is inside this code is wrong at all, I am just skeptical about how “new” or
effective this thing is, because I’ve been developing large scale software systems using the same approach, but with a
different name “DAO”, and so far I could not see anything different from this. I might let you guys twist my arms in the end of the book,
but so far I’m pretty sure the Repository and a DAO pattern are the same thing.
Any idea, suggestions, thoughts and counter-argument are very welcome !