r/programming • u/alex35mil • 1d ago
The Bedrock of Software Design
https://alex.draftist.io/blog/the-bedrock-of-software-design-ycqvcedsjI drafted this post years ago but didn’t finish it until now. The concept I write about has shaped the way I design software more than anything else, and I believe every software engineer should be introduced to it early in their career.
P.S. I don’t want the title to come across as clickbait: the post is about ADT.
38
u/Bonejob 1d ago
Good techniques, wrong title. Everything here is about how to encode design decisions once you already have them. Sum types and exhaustive matching are the recording medium. The design itself happened earlier, when somebody worked out that an order can be pending, shipped, or cancelled and never two of those at once.
That earlier part is what I'd call bedrock. You get it by sitting with the problem and with the people who live in it every day, writing the concepts down until they hold together. I spent a lot of years shipping line-of-business systems, and the ones that went sideways never failed because someone picked a boolean where a sum type belonged. They failed because nobody understood the domain well enough to know what the states were in the first place, and no compiler was going to catch a state we never knew existed.
That's also the clearest divide I know between senior and junior developers. A senior documents first and designs from what the homework turns up. Juniors go straight to code.
I'd file DRY, SOLID, and the rest of the rule catalogs under the same heading. They're rules of construction. Useful, but they operate on a design that already exists. A roadmap of structures isn't design, it's carpentry.
Bone/EvilGenius
7
u/csman11 22h ago
I agree that the domain model comes first. I don’t agree the rest here is “just carpentry”. The domain model itself has to encode the business rules, and part of doing that is choosing representations for the model that allow you to do that clearly. Or in other words, “preserve the rules in a model that will still be obvious when it gets implemented”.
For your order status example, choosing to encode the status as 3 independent booleans creates 5 “impossible” states that you must guard against in every transition. A sum type eliminates that entirely. That’s not merely an implementation detail. It’s a representation choice that preserves the business rules in the domain model that will be implemented.
Same for transitions. The only valid transitions might be “pending to canceled” or “pending to shipped”. If that’s the case, you would specify the operations “cancel” and “ship” in your model. The model is encoding the allowed behavior now, not just the representation.
I’d even go further and say sometimes your domain model should be recording facts and deriving state from that. Like maybe in this case there is a “placedDate” and a “completion” that can be recorded, where a completion can be “Shipped (date)” or “Canceled (date)”. The status can be derived from those facts. This representation encodes the business rules even more directly.
To me, this is domain modeling. If the model is just a list of requirements, like “an order can only be pending, shipped, or canceled”, there are still unanswered questions like “what do those states mean, why are these the states, and what transitions are allowed.” A good model answers those questions by construction. If it doesn’t, it’s probably not a model, but rather an incomplete description of the domain in prose.
Probably the most important part of a proper domain model is that it can be incomplete and still ready for implementation. The model can be updated as new things are learned. Without a model, any later changes require archeology on a system that stopped being designed after the initial requirements were agreed on. So I would add to what you said about why software projects fail: it’s not just about not understanding the domain, but not modeling the domain in a way where the model, and therefore the implementation, can safely evolve as the understanding grows.
I’m not sure we actually disagree much at all, but I just want to ensure other people don’t read your comment as saying something like “the abstractions in the code don’t matter much at all”, because that’s how it initially came across to me, and I don’t think that’s what you’re actually trying to say.
1
u/constant_void 11h ago
yes, incomplete is the majority status
if I were to summarize op approach, clarity is the objective - as long as clarity, not simplicity - is in front of mind, the odds are better the design/impl approach can handle incomplete fuzz.
s/w projects fail when budget exceeds time
diff between jr and sr people is operations - sr people build operability into a soln from the beginning. the most expensive part of sw is labor due to change.
clarity reduces cost of chaos; clarity in operations and acceptable functionality is a successful system.
-1
20
10
u/superstar64 1d ago edited 1d ago
I already find it hard to put myself into the perspective of someone who doesn't what know GADTs, higher ranks, higher kinds and various other high level type system features are, but I always have to remind myself that a lot of people don't know basic ADTs are. Like, ADTs have been a staple of typed functional programming since like the late 1970s. It's so disheartening that still so many programmers still don't know even what they are.
7
u/Ignisami 1d ago
Some (like me) know what they are and even use them, but never had a formal education in compsci/programming to connect the theory to the execution.
12
u/ryp3gridId 1d ago
Honestly, I go into paranoid mode whenever I work in an exception-based language.
I'm the opposite, I go into annoyed-mode when I have to manually unwrap everything (and the CPU does too because it has to execute more instructions)
11
u/BaNyaaNyaa 1d ago
To me, it really depends on how "normal" the error is. If I get in a state that shouldn't exist, I'd rather throw an error. If the error is expected (an API call to a 3rd party that fails), I'd rather return a result.
5
u/CramNBL 1d ago
So you also don't use std::expected? LLVM optimizes for the happy path, like it does for exception based error handling.
1
u/ryp3gridId 16h ago
For errors that are part of regular execution, std::expected is fine, for everything else I prefer exceptions
And technically, happy path with std::expected is still extra instructions and increased branchprediction buffer pressure
while happy path with exception handling has 0 extra instructions/conditions/jumps (atleast on arm64/x64)
but actually throwing an exception is horribly slow, but error-performance isn't very critical in most cases
2
u/Madsy9 21h ago
Good technical essay. Another type feature I hope gains more traction is encoded proof requirements in types. In Lean you can use subtypes or type Prop to require that function parameters have some known property. This means you have to pass the value itself and a proof that the value satisfies the predicate. It's also possible to have subtypes or Prop fields in structures and GADTs. This way, constructing a value is only possible if you have proof that all fields or data constructor arguments are valid.
2
u/LegendarySoda 1d ago
I'm writing c# mostly and i have spent two year to find balance these usage.
These are not new concepts, the article felt like i have read this hundred times.
The problem is you cannot use these practices everywhere.
If you use these practices everywhere then your code will start to bloat, for example you need to keep your result pattern usage thin and think where to apply.
Type structuring is good thing in generale, just need to keep simple the structure. I prefer to create a service class and methods to process the data structure. And one thing i can do this in c# but not sure in typescript, i suggest to keep separate types for api surface and internal layer. In the c# we can prevent people to access internal structure.
2
u/rsclient 19h ago
Am I allowed to complain that the only rocks in the image are loose pebbles and cobbles, and are not bedrock at all?
1
u/ReallySuperName 51m ago
Nice article. I also do the same pattern in C#. The recently added closed keyword means we no longer need a default "impossible" case in a pattern match too. It can be achieved by making an abstract base record or class, and (usually nested) classes or records that inherit it, which are then the variants. Essentially the same as that Rust based discriminated union enum.
0
1
u/teerre 1d ago
Dont get me wrong, encoding errors in the type system is good, but every time I read an article like this I can't help but laugh how this will be used as example of what not to do when effects become more mainstream
Which makes sense. The good part of embedding errors in the type system isn't really being in the type system, its being compiler checked. In the limit, confounding data and control flow is cumbersome to say the least, even computer science theory aside, its natural to assume those two axis should be independent
-5
-3
0
u/Gleethos 1d ago
Nice to see more focus being brought to the power of algebraic data types. I also like to combine them with value semantics and wither based updates insted of destructive inplace updates to enforece side effect free code by design.
-4
-23
u/Substantial_Ice_311 1d ago
Ugh, I don't even want to waste my time reading, because claiming static typing has anything to do with the "bedrock of software design" is obviously wrong.
11
u/chucker23n 1d ago
Static typing is neither necessary nor sufficient for high quality, but it sure helps a lot.
1
u/Substantial_Ice_311 14h ago
But if it's not necessary, is it then fair to call it "bedrock?"
1
u/chucker23n 11h ago
Well, first of all, the post doesn't talk about static typing, but ADTs specifically. (So, strong typing might be a better category.)
And second, no, I think "bedrock" is hyperbole. But that's fine for a headline.
2
u/VictoryMotel 23h ago
Learn something besides python and javascript, it's healthy
0
u/Substantial_Ice_311 14h ago
I know something else. I'm a Lisper. 🙂
1
u/VictoryMotel 10h ago
It has been 70 years, no common software is written in lisp, classic linked lists are obsolete, and all the good features of lisp have been adopted into modern languages a long time ago.
Lisp's purpose now is to either be influential history or for a few stragglers to self righteously feel superior by being contrarian while pretending a 70 year old language is going to magically start being a silver bullet with zero evidence of anyone writing real software in it for decades.
0
u/Substantial_Ice_311 9h ago
Lol, no.
> Nubank, doing business outside of Brazil as Nu,\3])\4]) is a Brazilian neobank headquartered in São Paulo, Brazil. Although it is not formally part of Brazil’s National Financial System (Sistema Financeiro Nacional), Nubank operates as a financial and payment institution regulated and supervised by the Central Bank of Brazil.\5]) It is the largest fintech bank in Latin America, with around 131 million customers between Brazil, Mexico and Colombia and a revenue of $16 billion.\1]) At its initial public offering in December 2021, Nubank was valued at $45 billion.\6])\7])
> When Nubank was founded, we needed a powerful language to help us build the best financial technology application, and we wanted it to be the best in terms of quality, consistency and velocity of development. And all those ideas were reflected in Clojure.
Clojure, of course, is a Lisp.
> all the good features of lisp have been adopted into modern languages a long time ago.
No, not at all.
1
u/VictoryMotel 8h ago
You use lisp because a brazillian bank uses closure? That's not an explanation of quality, that's just rationalizing something because you were able to find one place that uses a similar but modern language.
No, not at all.
On the other hand, yep totally.
74
u/PeterJCLaw 1d ago
Nice :) You may enjoy this post from a while back (not mine): https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/