Domain Driven Design Reference (2015) [pdf]
domainlanguage.com
domainlanguage.com
In my experience, the biggest hurdles are getting the business and engineers to follow the process.
Model->Validate with scenarios->Code Probe
Repeat as needed.
DDD lends itself to serverless and event-driven architectures and this is also a struggle.
Context mapping is important. Focusing on behavior is important. Identifying language is important.
Learning about Event Storming is also important.
But DDD is overkill for a straight data maintenance system (CRUD). The goal is to reduce complexity. Not create it.
Oh and the hard part. In DDD, abstractions and relational databases are bad starting points. You may end up with relational data stores, but the bound context should drive that. More often than not, a document data store is cheaper and more efficient. Abstractions are bad because we often see similarities in multiple boundaries and have an urge to combine things. But that’s a path to monolithic spaghetti.
I’m a huge advocate for DDD and believe many architects are stuck in PaaS. I think it’s critical to unwind that thinking.
A good example is how much greater would Salesforce be if they’d used DDD principles to design it? Clear boundaries. Clear modularization. As it is, it’s a tangled mess of monolithic code.
It’s very possible to start with products in mind and handcuff or cripple your modeling. You’ll often see architects constrain models based on acceptable technology, like only using SQL Server or always using RESTful apis. REST has nothing to do with business behavior. Using POST and GET exclusively and ubiquitous language exposes a more maintainable system.
If you’re forced to only use one kind of data store, just make sure you keep your boundaries clean.
I would love to see an example repo of a successful implementation.
[1] https://openedx.atlassian.net/wiki/spaces/AC/pages/663224968...
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.
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.
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.
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?
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.
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.
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.
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.
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
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.
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.
Many tales of woe (see https://rachelbythebay.com/w/2022/02/09/nice/ for a good sysadm one, or the syndicated loan problem in the original DDD book) could have been avoided by people striving to learn all they can. I guess though that this is tech's Eternal September: we have too many problems, so people want to minimize the amount of work required to understand a system.
Maybe things will be better in the future?