Hacker News new | ask | show | jobs
by moring 1515 days ago
Not OP, but the worst I ran into is the DB layer and the unit-of-work pattern. That is, loading an entity from the database, then implementing the domain logic at a high level (in-memory), and then you have to figure out how to translate this into DB updates AND make the whole thing concurrency-safe.

All the examples out there use UoW frameworks like Hibernate that fire lots of arcane magic at the problem that is near impossible to understand, show to be correct (even in an informal way), debug, ... The fact that you need "magic" at all is a huge anti-pattern to me. It just shows that the concept is flawed.

My own solution was to keep no entity state in memory and translate everything into DB updates immediately. Use DB transactions where needed. You'll need another layer of code to abstract those higher-level DB operations so your domain code isn't littered with DB accesses, but apart from the extra lines-of-code the whole thing becomes very clean IMHO. Problem is, you won't find this solution described anywhere, and you are completely on your own.

4 comments

There's a relevant term called "DDD Trilemma" (I like it because it's easy to find back articles on it). It states that you can't have both a pure, complete and performant domain.

For example, I think what you describe is opting to inject a service to the domain that makes it indirectly talk to the database. In light of the "trilemma", we can imagine it sacrifices a little bit of purity (external dependency, stronger tie to the database schema) in order to keep things simpler -- and nothing wrong with that. In some cases performance could be affected too (ie. if the natural logic might want to update an entity twice during the course of a transaction).

But you still have "completeness" in that you can query your domain as one unit and handle logic within it.

I like that the "DDD Trilemma" acknowledges there is no universal, perfect solution for handling the object-relational mapping problem.

I didn't know that term, so many thanks for that. I might add that I always treated performance in a "don't fix it if it ain't broken" way because clean domain code was much more important to me and performance problems tend to show up where you don't expect them anyway. One "escape hatch" I always had was to push operations down into the database layer if they can be solved by more complex queries or stored procedures.
Hibernate brings incredible flexibility and power but also the temptation to overuse it.

Funny how time and time again I've had the same problems of making collections lazy, deciding when changes need to propagate and so on.

Nowadays I much prefer a much less powerful and simple ORM approach. Yes ORMs are great and all, but really, mapping between objects and SQL is not really that tedious, and managing the lifecycle of those in-memory entities by the ORM itself is error prone.

It's ironic that actually working for banks made me give up all the perfectly matched, normalized and "elegant" ORM stuff.

> My own solution was to keep no entity state in memory and translate everything into DB updates immediately. Use DB transactions where needed. You'll need another layer of code to abstract those higher-level DB operations so your domain code isn't littered with DB accesses, but apart from the extra lines-of-code the whole thing becomes very clean IMHO. Problem is, you won't find this solution described anywhere, and you are completely on your own.

This is what I've settled on lately as well. I'm glad I'm not the only one. But, it's definitely not without its headaches. There are still domain entities/concepts, but they aren't always nicely centralized like in the typical examples of OOP DDD that choose to totally ignore/abstract persistence (and thus lead to bad performance, concurrency, etc).

Let me give an example of one of my struggles in a current project. We have a SQL database with pretty normalized tables. Some of the data in our system can't be actually deleted for legal/bureaucratic reasons, but nevertheless, end users will still sometimes want to "delete" some widget from their view of the system. As far as the end user knows, the thing no longer exists. So we use a soft-delete boolean column on these tables.

The struggle is that because our data is so normalized, we have to remember to account for soft-deleted rows of these tables all over the place. It's very easy to forget to filter your JOINs on rows that are "active" for one of these soft-deletable tables.

I've tried my best to write some helpers to make it easier to build some of these queries without falling into a rabbit hole of writing my own DSL on top of SQL..., but it would be so much simpler and less bug-prone to do it the inefficient ORM-style way where we just define an object that is the equivalent of `SELECT *` from a bunch of tables, and have framework "magic" create giant graphs of all of these objects every time we need to do a query+update.

A bit late for an answer, but an SQL view that only shows not-deleted data might have helped you in your case.
> My own solution was to keep no entity state in memory and translate everything into DB updates immediately

Did you encounter some part of the domain which was difficult to "push down" into SQL?

Also, how would this compare with encoding your domain code in a bunch of stored procedures, largely skipping a "conventional" programming language?

I tried to keep the domain logic in the upper layer and only push down "dumb" queries -- in the sense that the queries were allowed to be somewhat complex but not allowed to contain domain logic.

This way the DB layer was not written in a "re-usable" way because it contained specialized queries like "get all users that match this and that criteria", but they usually translated directly to SQL.

Encoding domain logic in stored procedures is something I didn't try yet. It would be very nice if SP weren't second-class citizen in multiple ways. But even then, at some point I guess there has to be an interface between the domain logic and SQL queries, so the problem isn't actually solved, just moved into the database.