You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
How much is the domain and service layer really decoupled from infrastructure concerns? #356
The design of much of the domain and service layer implicitly depends on the dirty-checking and automatic flushing mechanism provided by the ORM. For example, in chapters 2-4, the repository doesn't even have a save method. The service method allocate modifies one of the batches and simply assumes that there is some mechanism that will keep track of changed batches and persist those changes at the end of the uow. This makes implementing a new repository/uow for a new persistence backend that would fulfill the same set of responsibilities much more non-trivial than would initially appear.
In particular, if you implement the SqlRepository given as an exercise at the end of chapter two and try to switch to that repository from the code in chapters 3-5, they will find that all the unit and e2e tests pass but the application is still broken! The service finds the correct batch to allocate the order line to, but that change is never persisted.
A few comments based on these observations:
The passing of all unit and e2e tests and the application still being broken strongly suggests that we have made some implicit assumptions that we missed writing tests for.
Since our business logic was still quite tightly coupled to the ORM anyways, was there actually much point to implementing the repository pattern after all?
Considering that different repository implementations might have very different behaviors, is it even in principle possible to fix this problem? Since some repository implementations may have dirty-tracking behaviour and other not, the semantics of what happens if we modify an entity and don't save it will always be implementation-dependent, no?
these are great comments! how would you address these concerns? we need to extend the e2e test, and then experiment with a repository that has an explicit save() or update() method...
Keeps your domain clean – business logic never calls session.query() or manipulates models tied to the DB schema; it speaks to an interface expressed in domain language.
Improves testability – you can inject an in-memory or fake repository for fast unit tests without touching a real database.
Centralizes persistence quirks – eager-loading, caching, sharding, or soft-delete hacks stay inside the repository instead of leaking across services.
Supports future persistence changes – moving to CQRS read models, an event store, or adding a write-through cache only touches the repository implementation, not the domain code.
Clarifies intent – methods like find_available_trucks(loader_id, window) communicate business meaning far better than chained filter() calls scattered around the codebase.
Defines transaction boundaries – combined with a Unit-of-Work, the repository marks where data access starts/ends, making concurrency control explicit.
Provides a seam for cross-cutting concerns – logging, metrics, retries, or auditing can be added once in the repository instead of in every service.
@hjwp I think simply adding an explicit save method would not fix the problem because the semantics of what happens when you modify an entity and don't explicitly call the save method would still be implementation dependent.
One way to try to make the semantics of the different repositories similar could be to only ever return unmanaged objects, even from the ORM implementation, for example by explicitly detaching them from the orm session. The save method will then merge these objects back to the session. Even this approach has limitations - a, every save will trigger an additional, completely unnecessary select and b, it is not clear how to handle child entities. At this point, it feels like we might be better off having a completely different class representing the persisted entity and a mapper that converts to and from the domain layer (which is the pattern I have seen in the wild at least in the java world).
Honestly, considering the options, I have become much more sympathetic to the point of view that it is fine to live with some coupling between the persistence and domain layer. Considering how rare it is that you actually need to change the persistence layer, it feels like a perfectly valid trade-off, perhaps even the most pragmatic one.
100% agree. even in the ddd blue book, Evans says that ultimately your choice of database is going to influence your domain model, and that's a reasonable thing to happen.
still, your point about the behaviour differing depending on whether you use an activerecord-style orm or vanilla sql is a good one, and it's bothering me. i wonder if the UnitOfWork abstraction might be a place to lock down the behaviour? ie, if you don't do a uow.commit(), you can test that, no matter what repo you're using, nothing is saved?
Activity
hjwp commentedon May 13, 2025
these are great comments! how would you address these concerns? we need to extend the e2e test, and then experiment with a repository that has an explicit
save()orupdate()method...Bryant-Yang commentedon May 15, 2025
Chatgpt gives me these reason:
Why keep a Repository layer even with SQLAlchemy
Keeps your domain clean – business logic never calls session.query() or manipulates models tied to the DB schema; it speaks to an interface expressed in domain language.
Improves testability – you can inject an in-memory or fake repository for fast unit tests without touching a real database.
Centralizes persistence quirks – eager-loading, caching, sharding, or soft-delete hacks stay inside the repository instead of leaking across services.
Supports future persistence changes – moving to CQRS read models, an event store, or adding a write-through cache only touches the repository implementation, not the domain code.
Clarifies intent – methods like find_available_trucks(loader_id, window) communicate business meaning far better than chained filter() calls scattered around the codebase.
Defines transaction boundaries – combined with a Unit-of-Work, the repository marks where data access starts/ends, making concurrency control explicit.
Provides a seam for cross-cutting concerns – logging, metrics, retries, or auditing can be added once in the repository instead of in every service.
shitikanth commentedon May 15, 2025
@hjwp I think simply adding an explicit
savemethod would not fix the problem because the semantics of what happens when you modify an entity and don't explicitly call the save method would still be implementation dependent.One way to try to make the semantics of the different repositories similar could be to only ever return unmanaged objects, even from the ORM implementation, for example by explicitly detaching them from the orm session. The save method will then merge these objects back to the session. Even this approach has limitations - a, every save will trigger an additional, completely unnecessary select and b, it is not clear how to handle child entities. At this point, it feels like we might be better off having a completely different class representing the persisted entity and a mapper that converts to and from the domain layer (which is the pattern I have seen in the wild at least in the java world).
Honestly, considering the options, I have become much more sympathetic to the point of view that it is fine to live with some coupling between the persistence and domain layer. Considering how rare it is that you actually need to change the persistence layer, it feels like a perfectly valid trade-off, perhaps even the most pragmatic one.
hjwp commentedon Jun 27, 2025
100% agree. even in the ddd blue book, Evans says that ultimately your choice of database is going to influence your domain model, and that's a reasonable thing to happen.
still, your point about the behaviour differing depending on whether you use an activerecord-style orm or vanilla sql is a good one, and it's bothering me. i wonder if the UnitOfWork abstraction might be a place to lock down the behaviour? ie, if you don't do a
uow.commit(), you can test that, no matter what repo you're using, nothing is saved?