A lot happened this and last year, so why not to share some thoughts and experience from implementing DDD on a monolithic application.
Warning: it's just a high level overview and probably will not make much sense without experiencing similar issues.
I've been mainly working on an ERP system called TCIS for a Czech manufacturing company engaged in processing metal sheets and profiles. The system consists of several modules like Customers, Suppliers, Base Items, Stock, Deliveries, Reporting and others.
Technically, backend it's written in C#, .NET Framework 4.7, MS SQL database and Entity Framework as ORM, dependency injection everywhere.
To simplify typical common tasks used in forms (get list, display detail, update and save) we made a plugin to VS to design data using generators (dynamic data).
So the customer can even design and update forms directly by himself. All extendable with events of course.
The solution contains common patterns, like facades, services, repositories, separated data, business and presentation layer, all via interfaces and dependency injection, not bad at all.
Now after a few years as the system grows up and business requirements are changing over time suddenly the system became very hard to maintain. One service now calls another service in a different context. Adding more if-else clauses only makes the problem worse. Changing one part of the application results in braking another part. Have you already been here as well?
So we decided to split functionality into modules (domains) in a microservice style, but with all the benefits from monolithic application for small teams (one solution, easier deployments). But to succeed, we must be very careful: Don't touch code outside of your domain directly! But then, how to access data from another domain?
DDD and CQRS
Reading data is simple. Use queries to retrieve data from the database (either from views or tables) and transform into some DTO objects. Updates are a bit more complicated, but not too much. Since we don't want to use CRUD anymore, but strict DDD, there must be another DOMAIN layer to keep that logic.
Upon each change the domain object generates a domain event, which is handled by:
1/ event handler in repository - to persist changes to DB
2/ domain event handler - to perform another task. It can be whatever, some work on the same domain, enqueue a new integration event or something else.
Thanks to Jimmy Bogard and his Mediatr, which makes implementation very straightforward with Commands and Notifications. Many modules are now already separated and code is much easier to maintain.
Since we now have simple domain objects without additional dependencies, unit testing becomes a lot easier! No excuses anymore.
Thinking further. When all events have their own handles, why not to use event sourcing! The idea is it save the event first, then replay it. However, it's a bit overhead on this project and doesn't bring any customer value (apart of us geeks to try this juicy approach). We didn't implement it.
With an optimistic assumptions about importance of queues and amount of data being processed, I decided to build my own on SQL. After some time it turned out to be underestimated. I mean, it works fine, but not worth the effort and not bullet proof. I'd suggest to use some existing queue, even MSMQ, anything, custom queues will never be so perfect.
DDD is must simpler when starting from scratch. Implementing it into existing projects is very difficult.
Relational database is not the best option for DDD due to the fact, that it's the aggregate root, which is loaded and it may spread across many tables (yes, we can always create views).
On the other hand, NoSQL databases are designed exactly for this purpose and may be a better choice.
With CQRS you can always build read model in relational SQL.