Hacker News new | ask | show | jobs
by mvf4z7 1514 days ago
Has anyone here actually used DDD for building a significant production system? I have read a lot of material on the subject, and everything always sounds good in theory. But whenever I attempt to put the patterns to practice it seems like there are endless pitfalls and gotcha moments.

I would love to see an example repo of a successful implementation.

5 comments

Yes, find the entities/resources in the domain, model them, model their state and what makes them change state. Give names to those states and what makes them change.

Applied this to a public transport ticketing system, it has entities like "Patrons", "Tokens", "Trips", "Fares", "Passes", "Stations" etc.

Patrons can be registered or de-registered. Tokens can be Assigned, Unassigned, Blocked. Trips can be Entered, Exited, Completed. Tokens are used by Passengers (which are different to Patrons, think Parent buys a Token for Child that is travelling).

Model each of those Nouns and define the verbs that change them (eg Register Patron leads to a Patron Registered event).

DDD Smells: Words like "create", "delete", "modify". Betterer names are things like "register", "suspend", "assign", etc.

Focus on the Nouns and then the Verbs will follow. A lot of design seems to be on workflows and treat the Nouns as passive things that get changed.

I think of how it would work if people were doing it, not computers.

I want to open an account at a bank.

So there are two actors so far, me, and the bank. There's a Noun, an "account". What can an account do? Well, from that sentence, it can be opened. Who can open one? What is a name for me? Customer? AccountHolder? Client? etc. What are the details about the "bank", it has Branches and Ledgers and etc.

Note that absolutely none of what I've described above is about implementation.

Some of the gotchas are thinking that, for example, de-registering a patron is the same as deleting them. Developers love to model everything as CRUD, whereas CRUD are data operations, not a domain activity. Its very rare that you want to actually delete an entity. Accounts get Closed, but they don't get deleted. They get Archived.

That makes lot of sense.

Back in my student days we were taught about a role called "system analyst" I think it was part of COBOL. They have deep domain expertise and work with programmers to automate business workflows. Their job is to identify the entities you pointed out (patrons, trips etc.,) and clearly document all the interactions and workflows. They then figure out what kind of workflows need to be automated, to what extent and so on. They also are supposed to ensure that disruptions to existing systems/processes/people are kept at a minimum level. Note that these system analysts are not air-dropped product-managers but rather people who have been working inside the system for decades. They know all the edge cases, context, and more importantly history so they know why a certain Chesterton's fence exists.

I haven't come across such folks in my professional life. So engineers are expected to learn and automate a domain at the same time. It just doesn't work that way. No wonder many of the IT systems have been a disaster. Implementors discover, to their horror/surprise, that the real world is full of edge cases. Take Uber for example. The first launch didn't include tipping. It was a monumental effort to retrofit that into their payment processing and ride handling systems.

The interesting thing is that DDD identifies & distils most (not all) of the patterns I'd seen used or had to crystallize myself, over the past say 15 years of application development.

Key aspects like building a domain (business) layer, separating this from the DAO layer, speaking domain terminology, clearly defining entity identity (it's not a hash/ equals on Customer Name), encapsulating meaningful domain operations into domain entity & value types, orchestrating fine-grained data into larger aggregates, and orchestrating major business processes as 'services' are all great patterns.

What kind of pitfalls or gotcha moments are you running into? I would think the patterns above should suffice for business applications ranging up to moderately complex.

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.

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.

OpenEdx [0] the education platform is an example. Here is their design documentation showing their Bounded Contexts [1] in the DDD section of the docs.

[0] https://openedx.org/

[1] https://openedx.atlassian.net/wiki/spaces/AC/pages/663224968...

I’ve gradually started to use more and more DDD principles as my project grew in the 100s of kloc. Mostly to resolve pain points from natural evolution of the codebase.

For example, Repositories don’t make sense for a small project. But in a large Django project, it can be really hard to enforce sane prefetching of objects, since your QuerySet objects will silently trigger new DB lookups if they need to fetch data. So I found that defining a Repository that implements a few well-known query methods, does the right prefetches, and then Django-seal’s the QS is a nice way to encapsulate the smarts around DB fetches. As a bonus, doing this means you have a very clean stubbing point so that your tests can run without the DB, if that floats your boat (Useful if your domain models are running complex logic that could otherwise require lots of DB hits to fully test).

Aggregates are a useful way of solving the problem of picking a DB transaction boundary; combined with Repositories it makes it much easier to avoid deadlocks because you always start from one of the aggregate root models, instead of potentially selecting a leaf first, that might be the second query in another transaction. If you don’t need to prefetch a mode graph and deadlocks aren’t a problem, your system might not need an architectural principle like this.

Bounded contexts are the most valuable concept of them all; see Uber’a rediscovery of this concept as a way of taming their micro services complexity. It’s a great way of thinking about how to define your domain terminology, and how to talk about the boundaries where terminology should change.

I think DDD is particularly useful as a Schelling Point for architecture; it’s not necessarily the very best solution for every problem, but the value of having a very well-understood system that everybody knows is not to be underestimated. The alternative would be heavily documenting your own architectural principles, which might be better-suited, but are they so much better that it’s worth giving up all of the DDD knowledge and history?

Like all tools, it is better for some things than others. I think a failure mode is dogmatic adoption by architecture astronauts in systems not complex enough to justify it. But you can definitely just adopt the parts that you like and gradually migrate towards full DDD over time.

Ya, it scales very well to bigger project in fact.

The thing is, people do struggle to understand how to implement it and what it's all really about.

I recommend Vaughn Vernon - Implementing Domain-Driven Design for a good walkthrough of it in C#.

And I also recommend you look at my small demo example, which is in Clojure, but also walks you through what DDD is at a high level which should help you quickly grasp it (even if you don't know Clojure) in a literate programming form: https://github.com/didibus/clj-ddd-example