In domain driven design
- A repository is a collection like "interface" that hides data source access. It furnish add, remove and retrieval method just like a collection would. It does it using domain language.
- The application layer uses the repositories by thinking about them as collection, by retrieving from it or adding items to it, but should not be aware of whether a specific item is being persisted or not.
- domain layer should guard domain rules
With those rules in mind: How can entities updates be persisted and should the repository return self persisting entities ?
Example:
Domain
enum Status { awaitingApproval, accepted, declined }
class Order {
Status _status; // private
Status get status => _status;
// point 3. Protects domain rules
set status(Status updatedStatus) {
if (status != awaitingApproval) {
throw 'cannot update a $status order';
}
status = updatedStatus;
}
}
Application
// aggregate root
class User {
OrderRepository _ordersRepository;
Future<List<Order>> viewAllOrders() => _ordersRepository.selectAll();
// point 2. the application layer is not aware of whether the order update is persisted or not.
Future<void> acceptOrder(Order order) => order status == Status.accepted;
}
Repository
class ConcreteOrderRepository {
Future<List<Order>> selectAll() async => inMemoryStorage.selectAll('orders');
// point 1. No update method on a collection like object.
}
There is still something missing here: How is the update to the Order
being persisted ?
I've seen two version to resolve this architectural issue:
- add an update method the the repository which has as side effects:
- You can't think of the repository as a collection, it is now an abstraction over persistence. This is imo a very distinct concept of the original intent of repositories.
- The application layer is now aware it is using persistence as it has to explicitely call
orderRepository.update(order)
..
- The second solution I've seen is to use an ORM framework and adding a bunch of annotation in the domain layer.
@Entity()
comes to mind. I believe this is the worst solution of the bunch as it polutes the domain layer.
A third solution that I've not seen is to return a PersistedOrder
instead of an order to the application layer:
class PersistedOrder extends Order {
final OrderDataSource orderDataSource;
factory fromOrder(OrderDataSource dataSource, Order order) {
return PersistedOrder(dataSource, status: order.status)
}
@override
status(Status status) {
super.status = status;
orderDataSource.updateOrder(super.id, this);
}
}
class ConcreteOrderRepository {
final OrderDataSource orderDataSource;
Future<List<Order>> selectAll() async {
final allOrders = orderDataSource.selectAll();
return allOrders.map((order) => PersistedOrder.fromOrder(order));
}
}
This seems to be in line with point 1, 2 and 3. Only the Persistence layer knows that the Order
is in fact a PersistedOrder
, other layers just use an order and an orderRepository as a collection where they can retrieve and add elements.
Still, somewhat of a red flag for me is that I've not seen anyone do this, so there might be a reason. Which is what this question is about:
- Have anyone encountered this pattern ? Does it have a name ?
- What would be / are possible draw backs here ?
saveChanges
does not obey rule 2 in this post. – Ced Aug 04 '22 at 17:35Save
method, which decides by itself whether there is anything to save (e.g. did any value actually get changed? Is this user allowed to make changes? ...) This satisfies the definition of DDD whereby the application layer is not the one making the final decision on whether to save or not; and yet the application layer is still calling aSave
method. I think you're conflating a suggestive name with an actual technical implementation, but these are two very different things. – Flater Aug 04 '22 at 18:25saveChanges
: To me it sounded like "update x, then y, then verify result is valid, then persist", which would contradict another ddd view that the application state shouldn't ever be invalid. – Ced Aug 04 '22 at 19:51bool canExecuteOperationOn(order, tenant, whatever)
. Whether the update is called on an active record or with saveChanges, I don't see how that differs. – Ced Aug 04 '22 at 20:12