This blog post is all about: the common part of "Shallow DDD" & "Aesthetic Clean Code", what really is Business Logic (& why you may be wrong about it ...), what part of BL is really within your "algorithmic" (imperative) code and where you should really apply your focus to if you want to improve Business Logic.
Disclaimer: we're using code to create all sorts of applications, following radically different paradigms - it's not possible to find a common denominator for all kinds of apps. This blog post focuses on a typical, web, interactive, user-facing OLTP-sort-of applications (because of how popular those are).
Looking at the recent trends in software engineering, two names come to my mind:
- "Shallow" Domain-Driven Design (SDDD)
- "Aesthetically" Clean Code (ACC)
Both are equally dangerous, both have great intentions, both completely miss the point (real problems to be solved). But they have enough publicity & very vigorous evangelists, so they spread like a disease. I'm not going to dissect them into pieces here (sounds like a decent idea for another post(s) ...) - the reason why I refer to them is that their purpose is to fix/improve/clean so-called Business Logic. Which IMHO they don't.
The purpose of this post is to explain why (and what to do about that).
What is Business Logic?
Yarrr, it's definition time!
Business Logic is collection of rules, control flows, constraints, laws and behaviors defined by your domain model that are to be reflected (implemented) in the solution. Business Logic is all about defining the boundaries between what's necessary (expected), allowed & restricted (forbidden).
Someone has said one day that Business Logic is an outcome of processing of functional requirements (.../expectations/desires/needs) - it's quite well said as well.
Both SDDD & ACC have set themselves a goal of "extracting" or purifying Business Logic. To decouple it from dependencies like relational database, cloud platform, or external service provider (the one you integrate with). Hence all the hexagons, repository patterns, excessive IoC, ... Reminiscing about many developers I've met - this "decoupling" or "cleaning" seemed to take 50%+ of their work time (& at least 75% of their focus) ...
What REALLY is Business Logic ...
No, I'm not going to revamp the definition above - in fact it's quite OK. The problem is in how we INTERPRET it. We're wrong in thinking that separation of code and the database it operates upon (the most frequent case) means keeping Business Logic out of Data Persistence (and hereby separating the concerns). We think it's "decoupling".
Well, apparently we're wrong.
Business Logic ain't just "ifs", "fors" & "whiles" in the code. Massive chunk of Business Logic is ALSO reflected ...
- in defined types (because they are constraints as well)
- in data models/schemas/relational tables (what belongs together, relationship between data - it doesn't have anything in common with "relational" aspect of RDBMSes!) - whether we like it or not
- in contracts - what we allow at "access points" (e.g. in APIs)
- in code structure & hierarchy - how to split part of processing between "modules"
- in names & conventions - because they also influence how we perceive & understand semantics of the code
I'd say that Business Logic is SUM of EVERYTHING behind a particular contract (of area/module/component/whatever) as far as referenced contracts of other areas/modules/etc.
Those elements of the SUM (code + types + models + schemas + ...) are not "coupled"! They are supposed to work in unison (& always together!) - removing some of them (e.g. data schema) cripples (limits) the SUM. These things BELONG together and it's not a negative thing - quite the opposite! Pretending the do not & just "cloning" the same rules across the "layers" is just unnecessary code duplication that cripples the Developer eXperience (DX) - without meaningful justification (other than twisted "aesthetics").
Strip it (anyway)!
And what does happen if you decide to strip the code of all the other "dependencies" regardless of all what just have been written above?
To answer that question, let's revisit the paradigm of modern web-facing OTP app:
- it should use lite, stateless, independent requests - so we read only what we need to deal with the request & persist all the mutated state after the request
- transaction (consistency) boundaries do not span across more than 1 request
- when it comes to data heavy-lifting (searching through & scanning vast information), what makes sense in 99% of cases is let the DB to handle it (because it's so optimized for fast scans, seeks, joins, etc.)
What does it leave us to do in our newly purified dependency-less Business Logic Lite (that meets the paradigm described above)?
- basic filtering small volumes of data (e.g. exclude items of status "open")
- merging/joining/grouping small volumes of data (e.g. add buyer info to transaction record)
- simple arithmetics (e.g. calculate age based on data of birth; subtract discount value, add tax and calculate %-tage based commission on total)
- simple mapping (e.g. map status value onto 1 of 3 user-friendly values)
There's really not much more. Simple (if not trivial) stuff. Even if we have some more sophisticated business logic, we usually help ourselves with smart data redundancy, to simplify the processing.
What's my point here?
Sorry to wreck your naive hopes, but ...
Your business logic will ALWAYS be strongly reflected in your data model (& your persistence) - trying to "decouple" it is a grave mistake. Why? It's not coupling, it's COHESION. It's not a different concern - it's part of your BL. All efforts to extract "core", "domain" or distinguish "domain services" from "application services" are a waste of time - cause massive duplication, a lot of boilerplate that ends as error-prone pass-through code (that doesn't add any value).
To put it bluntly: we're speaking about over-engineering (that generates massive future costs) here.
Instead of playing with putting ORM code behind repository interface (which is my definition of insanity ...) or building some other hexagons ... you should:
- identify functional (/domain) boundary (addressed concern, key abstractions of an area)
- shape a ubiquitous language-expressed contract for this area
- put ALL the business logic that belongs within this boundary inside this area, regardless of whether it's an algorithm, DB access mapping or remote contract calls
If your service DOES depend on DB (the logic you've put in it), using smart tricks (like DI) doesn't change this fact, it gives you the illusion - ask yourself honestly what does this illusion provide you? How does it help? An option to switch from SQL DB to No-SQL DB by re-implementing all the repository interface implementation? How many times did you need/do that in the past?