The previous episode in the series (part 3) can be found here.
It's the time. First, to conclude this short series of blog posts dedicated to what's wrong with DDD. Second (and more important) - to talk about a practical alternative to DDD. Is there any? How does it solve fundamental issues of DDD? Doesn't it come with its own set of drawbacks?
Let's find out.
As there's no point in re-inventing the wheel from scratch, is there anything I'd like to cherry-pick & keep from Domain Driven Design? Yes, there are a few high-level concepts I find very useful: domain (as a problem space), three sub-domain types, model (as a solution space), and Ubiquitous Language (as a set of terms used in both domain and model).
That's already a big chunk of what we need. But not all of that. That's why I'd like to introduce a bunch of new terms:
- capability
- contract
- concept
- connascence
That's all we need. By sheer coincidence, they all start with "C" - but that was not intended 😉.
CCCC > DDD?
Capability
A capability ("the bag of goodies") is a subset of the model (with an accompanying implementation) with a defined purpose. "Defined" in what way? A goal/mission to fulfill, a problem to solve, or a concern to address. Capability is revolving around one or more pivotal concepts. A capability has clear boundaries - a particular piece of functionality (e.g., function) cannot belong to more than one capability unless nested. Ah yes, you can nest capabilities - this is how you hide complexity behind abstractions.
Examples? If our domain is about food delivery, some of our capabilities may be: offer catalog (where providers can define their assortment), pricing (everything about prices, incl. such elements like taxes, delivery fees, or discounts - which all may be nested capabilities), settlements (actual flow of money between involved parties), marketing (all ways for the providers to promote their offer), loyalty program, provider reviews, order fulfillment, etc.
Contract
A contract ("the portal") is how you interact with capability. It's a set of operations/data explicitly published by the capability owner for others to use. It can be limited (who can use it), it's pretty formal (to eliminate ambiguity), and it's phrased using concepts (to avoid technical implementation details leaking out). Publishing a contract is a duty with certain obligations toward its consumers.
Examples? I'd imagine pricing capability exposing such operations like: setting a base price for the offer item (effective now or at a given date), setting up a discount price for a given period of time or with specific conditions (e.g., only first order), or setting a special price for a group order (of a full set). Please note the fact that: (1) aforementioned operations are business operations expressed in UL; (2) the way they will be defined in the contract is actually strict and formal - a precise list of required, named parameters (preferably - concepts).
Concept
A concept ("the anchor") is a term from UL with a clear, unambiguous definition phrased in UL (possibly using other concepts).
Examples? Here we go (again, from the pricing capability): base price, total price before discounts, price to be paid, quota discount, percentage discount, etc. Each of these concepts should have a clear (documented) definition. The definition can be based on/referencing other concepts - but in an unequivocal way (no flowery synonyms, please!).
Connascence
Connascence ("the magnetism" - a generalization of two concepts, coupling and cohesion) is not something you design upfront. It's a measure of how two particular concepts go together closely. To "go closely" is a compound metric (code-based) that takes into account, i.a.:
- How frequently both concepts are used together (e.g., in the same algorithm)
- If one of the concepts depends directly on the other one (e.g., class inheritance, object composition)
Examples? As you wish. The base price has a high connascence with quota discount (as these both are related to a particular offer item) and the price to be paid (as the former two are ingredients of the latter). The commission fee has a slightly lower connascence with any of the aforementioned - it's not an element of the price, but an element of settlement - however, it's still based on the price of individual items. The volume-based commission has an even lower connascence with the original three concepts - it's not related to any single offer item but to the total volume of the sales (of a provider) within a period of time. However, there is a high connascence with a commission fee because they are both ingredients of the monthly settlement (between the provider and platform owner). Makes sense? I hope so.
How to use CCCC
So, what we have at this point is a very abstract, high-level "framework" to phrase a model in a universal yet scalable way. CCCC should work with the majority of programming languages (be mappable to their code constructs) and still - remain understandable for non-developers.
But how to use it in practice?
The first three terms from the list above (capability, contract, concept) - CCC_ - must be ALL present not just in the model but also directly in the code - via annotations, metaprogramming, decorators, etc. To be explicit - the code constructs that represent them have to be marked explicitly so this association is VISIBLE (& enables the connection between the code and external model documentation). The exact technique will depend on the tech stack used. You'll need some conventions and static check tools (probably customized a bit) to ensure that the CCC_ information accompanying the code is (and remains) correct - e.g., the concept mentioned in the documentation really exists (in code).
Here are some examples:
- in C#, one can use custom attributes
- in Java, custom annotations
- in Python, class decorators
- in TypeScript, reflect-metadata library, and decorator metadata
Capabilities do not have to be expressed with annotations but can be deduced from the code structure (at least for the majority of programming languages) - nested directories, defined modules, etc.
Contracts are protocol-agnostic, generalized API definitions. So, you can maintain them in code on the level of interfaces (C#, Java), abstract base classes (Python), behaviors (Elixir), etc.. Or use some formal IDL specification of your choice: OpenAPI, RAML, Avro, Thrift, Smithy. The goal is simple - ALL capabilities should be reaching out to each other via contracts ONLY.
The final C in the gang - connascence (___C) is a control measure that verifies our composition:
- The ___C for any two concepts that do NOT belong to the same capability should be low ("coupling")
- The ___C for any two concepts that DO belong to the same capability should be high ("coherence")
How does CCCC solve DDD deficiencies?
Capabilities are the hierarchical building blocks of our solution. They are the unit of ownership (should be owned explicitly by someone) and the unit of business logic, but not necessarily the unit of deployment. The way you structure your capabilities should have an impact on how you structure your organization (Conway's Law). Needless to say - your (UL-phrased) documentation should also be organized according to the capabilities' "tree."
The hierarchy of capabilities should be visible directly in the code (that's how you navigate the model space directly in the code). When we check a class/unit/module/whatever, we should immediately know which capability we're within.
It should be possible to annotate code constructs within the capabilities (with comments, clarifications, examples, diagrams, and explanations), and such annotations should be a crucial part of auto-generated documentation (in some browsable format) - this can be achieved with tools like XWiki (with Groovy), DocFX (with .NET), or Sphinx (with Python).
This part is NOT negotiable! That's how we keep the model in sync with the code. That's how we make sure that the solution (in programming language) is an implementation of the model (same capabilities, same concepts), not just some undocumented "interpretation" of developers.
Contracts are taken straight from the Promise Theory. They are like APIs (when it comes to your obligations and how responsibilities are split), but there's no assumption of being called remotely or even of the exact protocol in use. Contracts can be used within a well-structured monolith ("modulith") for inter-module "calls". They are necessary to govern dependencies between capabilities (aka keeping tight control over coupling).
Properly defined contract definitions could also be used for:
- Contract testing/validation
- Documentation generation (obviously)
- Creating usage examples
Having Concepts marked explicitly in code (e.g., as classes/objects/structures) brings several advantages.
For instance, it makes it easy to ...
- ... identify code not related to any concept (potential fluff/toil/debt)
- ... navigate between the given concept in code and the same concept in the accompanying documentation (e.g., in the developer portal)
- ... spot outdated/duplicated/ambiguous concepts ("model debt", drift between model and code)
Last but not least, automatically calculated connascence provides you with the necessary feedback: does the intended (upfront designed) composition work properly, or has the "real life" proven it wrong (because the code you're writing follows a different "unofficial"/chaotic structure)?
If the connascence indicates an issue (low cohesion or high coupling), you need to act upon it: revise capability boundaries, split too capacious concepts, merge synonyms, adjust contract definitions, etc.
Parting words
All this is NOT what you may call a ready-to-use, step-by-step recipe. Some additional work is definitely needed to make it work with your tech/knowledge stack (the programming languages you use and your knowledge management tools). The even more tricky part is the governance: synchronization+validation mechanism that will help you keep the model and code mutually up-to-date.
The way ahead to achieve that won't be all roses - you'll for sure find some edge cases and tricky idioms that won't fit the proposed conventions. It will take some time to work out a decent approach to measuring the connascence (simple enough, w/o overfitting). This will require collecting a lot of feedback, running several hypotheses, and most likely customizing "the framework" to your ways of working as well - it's going to be an iterative process, not a one-off project to execute.
To help you with that, I've created a mini-F.A.Q below. I'll be happy to expand it with any questions that will pop up in the comments.
Mini-F.A.Q.
Q1: Data! What about the data? Do capabilities "own" their data?
A1: Capabilities are logical composition units (of your solution), and data tables are tech stack-specific implementation constructs. Just like functions, classes, triggers, or whatever else you're using while developing. In general, all such constructs should belong to some capability.
Q2: What are the practical implications of nesting? E.g., does the "parent" capability have to interact with the "child" via contract? And vice-versa? If a concept belongs to the "child" capability, does it also belong to the "parent"?
A2: All inter-capability interactions go via contracts. My recommendation is to distinguish two types of contracts: "external" and "internal" ones. "External" is for anyone to use, "internal" is only for your parent capability and sibling capabilities (children of the same parent). It's a simple convention that doesn't really shackle us much but helps tremendously in organizing scalable business logic.
Q3: I'm not sure I really get "concepts". So, if the concept A belongs to capability X, I can't use it in Y?
A3: No, that's not how it works. Let me illustrate it with an example. There may be one capability called "User registry", with the idea behind it being "everything related to the lifecycle of the user on the platform". One of its main pivotal concepts is obviously a "user", and in the contract exposed there are such operations like"registering a new user", "resetting the user's password", or "archiving a user after a period of inactivity". However, there are many other capabilities that will likely REFER to the concept of "user" - e.g.: "Sales" may want to "list all the transactions for a given user", or "Reviews" may contain "adding a review on behalf of a user". So, how should this work in practice?
- user-related functionality that belongs conceptually to the "User registry" capability should remain there and should NOT be duplicated
- it's OK to create user-referring functionality in other capabilities if it has a higher connascence with concepts in those capabilities
- if functionality in other capabilities wants to interact with the "User registry" capability, it should do it via its contract
Q4: Contracts do change over time. Does CCCC tell anything specific about contract versioning/compatibility?
A4: Nothing specific. Consider using well-known patterns for APIs (e.g., semantic versioning (for external dependencies), keeping it stateless, avoiding breaking changes with backward compatibility, etc.).
Q5: Is "concept" the same thing as DDD's "entity"? Does "concept" have a unique identity? What can we assume about it?
A5: "Concept" is a concept 😄 - nothing more, nothing less. Some of them will have an identity ("user"), while some of them may not ("availability" of a provider to server customers). Identities may differ a lot as well - starting with technical IDs and ending with the ones of business meaning (credit card number, personal ID number, etc.).