Do you feel like I do, and think everyone has their own take on Domain Driven Design (DDD) ?
Stop for a second and try the following exercise with me.
Forget about the words "domain", "repository pattern", "DDD", "adapters", "hexagonal" and "dependency injection" for a second.
Do you find yourself in the swamps of pointless code indirection and useless inconsistent abstractions that just hinder you ?
In this document I propose a way of designing and reasoning about software that takes learnings from Domain Driven Design (DDD) but purposefully re-brands them, so as to avoid the multiple meanings that certain words have acquired as they degraded, a.k.a. "historical baggage".
As software engineers we try to design systems in a flexible and maintainable way, and there are lots of methodologies with the same goal. The System Interaction Model (SIM) is just one more of them.
Think for example in the effort it takes for changing your program from saving data in Redis to PostgreSQL, or from hosting a REST API to converting it into a CLI app.
In software, ever changing business requirements mean we must be flexible. With good design and if you stick a bit to these guiding principles described here, this can be done with much less pain.
An added benefit of this model is that your interactions become very clear, succinct, and easy to reason about.
Heck, even your Product Owner and other non-tech people will be able to read and understand them.
SIM "System Interaction Model" is a way to write software with good design in mind.
For this, a somehow onion like architecture is used, by separating code in logical layers within a system.
This creates powerful and expressive abstractions, preserving a great separation of concerns, and extreme system correctness and flexibility to time and changes, specially if using the type system of the language to your aid, establishing clear interfaces, contracts, data models, type-classes, etc.
In a nutshell you could explain SIM to a heretic in a couple sentences:
Combine DDD with a DSL at the center, rename most parts of DDD, separate use-case from its constituent parts.
Wiring is application level, Interpretations are just implementations, a.k.a. infrastructure-level.
No repository pattern, Yes system capabilities and abstractions.
This is a model that defines principles and a philosophy which makes it much easier to reason and develop complex software systems in any programming paradigm, using a ubiquitous "domain" language, and overall makes it simpler to maintain and change underlying implementations and implementation details without having to affect the "business logic" (interactions) or core data models of your systems.
Throughout this document we will make usage of the analogy of Dunder Mifflin's e-commerce platform, with shopping cart, paper products, categories, etc.
SIM promotes the use of expressive abstractions, loose coupling, and strongly focuses on using type safety as a design cornerstone, with often beginning development of something with defining a data type, or a DSL (domain specific language that captures all operations possible in the "system"), then using that DSL to write your interactions with the system.
SIM emphazises and motivates the use of FP "Functional Programming" techniques like Monads, Free Monads, Semi-groups, custom Algebras, Lambdas, Immutability and much more. It has been proven time and time again that ths leads to more expressive, concise, and generally more correct software.
Notice we purposefully are against the usage of misconception-ridden words, like "repository pattern", "domain-driven", and other buzzwords. After years of experience with trainwrecks that were meant to be DDD, I honestly think SIM takes all those good parts of the "current practices" of software, and elevates them to new heights.
System is the core of your piece of software. Here you define all possible interactions with the system, create, read, update, delete, do this, do that, commands, queries. Also the data models required for it.
This part of the software defines a DSL, or simply data models, traits, interfaces, some small helper functions, monads, algebras, free monads, or even any other more expressive ideas, it's up to you.
Interactions are your use-cases of the system. This is what you want your software to do, and where you define the rules and execution and control flows. You could somehow see this as the "Controller" in MVC patterns, or as the "business logic" / "domain logic" in any number of other paradigms.
Describe your interactions with the system ideally in high-level DSLs, without coupling yourself to the underlying nitty gritty implementations.
This is an ideal spot to use the free monads, algebras, DSLs, traits, etc. that you defined in the system definition.
Interpretations or Implementations are the parts of your software that define the actual code that will implement a certain feature of the system, satisfy a certain trait, extend a certain interace, provide a free monad interpreter.
This is also known as "adapters" in certain other paradigms. Here you write your PostgreSQL queries, you Elasticsearch requests, etc.
Designing software should begin by the abstractions and ease of use. The way you write interpreters/implementations should depend of the core system, and not the other way around.
Wiring means combine the desired interactions with the system, with the right interpreters, and provide a way in and a way out to the program.
Every useful program is I/O bound, meaning it will have a way to receive data (CLI arguments, commands, HTTP body) and a way to do some effectful action with it (print to console, log to file, HTTP response).
A good first step is to try to separate your system definitions from your interactions, and move interactions away, in a way which they have as little dependency as possible on the actual implementations.
A good move is also to check that you have a clearly defined separation between implementations and the actual business logic. That will enable you to move easier to SIM.
A prime example of SIM can be found in the WikiMusic API project, designed with SIM in mind from the start, and making use of free monads and interpreters: https://github.com/jjba23/wikimusic-api
SIM takes many forms and knows many ways. What is important is that you follow the school of thought, not how you named a folder.
Too many people think they do "Domain Driven Design" because they have a
domain
folder in the codebase. Trust me it's not.
See below for an example project structure for Scala, for our e-commerce program org.dundermifflin.ecommerce:
sys
is a package where we define our
system, in terms of a DSL/Algebra (system definition) in the
form of trait, and some ubiquitous data models, alternatively
would have called it free if it was a package with free monadic
definitions of the possible interactions with the system.
interaction
is a package containing the
interactions with the system, our business logic, agnostic of
interpretations/implementations.
model
is a package with common data models and
validations to the entire program.
postgresql
is a package with the PostgreSQL
implementation of several Command and Query interactions (reads/writes
into the database).
opensearch
is a package with the OpenSearch
implementation of several Query interactions for search
suggestions.
smtp
is a package that contains SMTP Mail related
implementations.
http
is a package that contains the API definition,
routes and wiring of HTTP.
console
is a package with functionalities and an
interpreter related to logging to console / stdout.
Boot.scala
is the entry point to start a REST
API.
Prelude.scala
contains a custom tailor-made standard
library (prelude) for using in the codebase, which helps
developing.