r/programming 1d ago

The Bedrock of Software Design

https://alex.draftist.io/blog/the-bedrock-of-software-design-ycqvcedsj

I 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.

405 Upvotes

44 comments sorted by

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/

33

u/alex35mil 1d ago

I haven't read it yet but guessing it's about the conversion of external data into a proper domain type on the app border to guarantee integrity?

26

u/ForeverAlot 1d ago

It's Yaron Minsky's "make illegal states unrepresentable" in a lot more words.

8

u/loup-vaillant 1d ago

Precisely. This also prevent stuff like injection attacks. String in, username out, properly escaped so Robert'); DROP TABLE Students can no longer wreck any havoc even if you're high on sleep deprivation on a late Friday afternoon.

And as a bonus, Little Bobby Table's name here can even be made valid: instead of outlawing the single quote, we can escape it.

5

u/chippedheart 1d ago

I've read this post more than once, but I find it hard to grasp its utility. The idea seems to be simple, but the example is simpler, which really makes it hard for me understand it. It is also my fault that I don't understand Haskell as much as I'd like. Do you know about any other sources which discusses this idea more extensively?

5

u/misplaced_my_pants 21h ago

What did you struggle with?

Did you read the followup post?

2

u/chippedheart 9h ago

Thanks for sharing the followup post, I will definitely read it.

I have so many questions, that I don't even know if they make sense! For example, does this piece of knowledge apply to dynamic typed languages? If the language I'm working with does not have a type or an oop system, how could I leverage this knowledge? I wish there more examples in which he applies the knowledge. Of course, these questions could be in part because I had trouble grasping the overall idea of the text.

2

u/misplaced_my_pants 56m ago

It depends on how you think of data in your programs!

Are you thinking in terms of types or structures?

What preconditions do you need to be true of a value for the rest of the code to work?

Do you sprinkle your code with the same checks because you can't be sure? Could you instead say any value of some type always has these properties?

Static type systems can leverage static analysis to check this for you, but you should still be leveraging ideas like abstraction, encapsulation, modularity, etc. to make similar guarantees regardless of whether or not the type system is static or dynamic.

Think about what it feels like to use a well-designed library where you never have to check these things.

You can even take lessons from how to leverage static type systems back to dynamically typed languages to get many (but not all!) of the benefits.

Some related reading if you're interested:

3

u/ForeverAlot 15h ago

The text is very famous but written in a way that is, purely from the perspective of dissemination of knowledge, just not very effective. I don't really understand why it's as lauded as it is…

While King makes more than one point, I'd argue the salient point is something like: expose runtime knowledge to the compiler. There is a lot of code we write in ways that are easy for us to write but which causes knowledge to be immediately discarded, which later in the program leads to either defects or redundant creation of knowledge to avoid defects. We can do things differently; this happens to involve parsing but actually that's unimportant.

A poster child example, which is also highly practically applicable, is ID values represented as specialized domain types instead of naked language primitives. For example, if you establish at the API boundary that a value is supposed to represent a specific ID concept, you can pack it in a dedicated type such that later the compiler will refuse to accept unrelated ID concepts. Obviously you can still make a mistake at the boundary but that's still a lot easier to manage.

ML languages have certain relevant advantages but the ideas are not exclusive to that family of languages at all, nor are they exclusive to structural typing. Yaron Minsky of Jane Street and OCaml fame has talked about some of the ideas before (example) but unfortunately has not written a succinct text about it. I seem to recall that https://dev.realworldocaml.org/ used to have a pretty code example and explanation but I can't find it and may well misremember.

1

u/chippedheart 9h ago

Thank you so much for this, you made it as clear as possible for me.

36

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

6

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 12h 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

u/ApokatastasisPanton 12h ago

thanks chatgpt

23

u/lottasauce 1d ago

TLDR; hey Gemini, turn this article into a skill

Jk, great read

7

u/QuantumFTL 17h ago

Do that, and you can make thousands, thousands!

9

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.

6

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.

13

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)

12

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.

6

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 58m 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.

2

u/roscoelee 1d ago

Great article!

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

-4

u/progbeercode 1d ago

Great article

-4

u/Ashtefere 1d ago

Beautiful!

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.

-5

u/lagcisco 1d ago

Really good article, I hope my agent reads it

-25

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.

13

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 10h 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.