Scale complexity in software applications
Sandro Maglione
Get in touch with meWeb development
28 February 2024
•6 min read
Sandro Maglione
Web development
Learn about a new paradigm to implement software applications at scale, using the type system to drive development:
- Return errors as values and collect them on the return type
- Define and compose services in layers using dependency injection
- Use the compiler to fix errors before releasing the app
This new way of building software is what Effect is all about 👇
Software is complex
At the beginning it was all javascript, and it was a nightmare:
No types, anything can happen, no checks, no help. No way to manage complexity at all.
Some smart folks created types:
Great, but not so helpful. We barely know the parameters and what the function returns, but nothing else. Not much help in dealing with real-software complexity.
Most programmers are stuck here. No good 🤷🏼♂️
Functional programming to the rescue (really?)
Some other smart folks (mathematicians) invented functional programming.
New ideas and types to scale complexity: Either, Option, Functor, Applicative, Monad...
Wait? What is this? We are no mathematicians, what is a Monad? Or better, who cares?
Unfortunately, the tools and terminology behind "pure" functional programming is scaring programmers away (and they do have a point).
Complexity at scale
The solution is to make the complexity explicit, understandable, and readable, without jargon, all using types:
The return type defines everything that can happen:
PaymentReceipt
: If the function succeeds we get back all the information about the paymentConnectionError | PaymentError | WrongParametersError
: If the function fails, we know exactly why and where, so we can respond accordinglyPaymentService | AuthService
: A complex system is composed by many moving parts, the type reports all the services required to make a payment
How is this helpful
Types become our real co-pilot:
- Enforce providing all required services
- Enforce handling all errors
- Enforce creating valid response types
All possible issues are managed and propagated by the type system:
There is more.
Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 600+ readers.
A problem of try and catch
"I don't get this, how does this help to scale complexity?"
Back to the origins, what can go wrong in the code below? 👇
After you read the implementation you spot a throw
for an invalid card. Is this enough?
Not really. What about getCreditCard
and getIsCardValid
?
Well, we don't know. The response type doesn't help.
We are required to read (and understand 🤯) the internal implementation of both
This is a recursive problem: for every function call we must spot every throw
to handle all errors properly.
Solution: wrap everything in try/catch and call it a day 💁🏼♂️
Or even better 👎
We can call this global reasoning: you must read and understand all functions implementations to manage complexity
Local reasoning
Guess what? There is a solution for this:
The errors from getIsCardValid
and getCreditCard
are propagated and collected inside makePayment
.
Even better: we do not need to know the actual implementation: the type signature is enough 💡
This scales complexity: we can focus on each implementation knowing that all errors and dependencies are collected and checked by the type system.
We call this local reasoning: you focus on the implementation of single function, and then compose them together.
No need to read any other function implementation, the type is enough
This is what Effect is all about.
Effect is the missing standard library for TypeScript: it provides a solid foundation of data structures, utilities, and abstractions to make building applications easier at scale
If you are interested to learn more, every week I publish a new open source project and share notes and lessons learned in my newsletter. You can subscribe here below 👇
Thanks for reading.