Principles of Software Engineering: Choosing the correct programming paradigm for your project

Welcome to Principles of Software Engineering, where I, Jon Crain, self-taught programmer and ex-Facebook employee write about Software Engineering.

Today we'll be talking about choosing the correct programming paradigm for your project. We'll discuss object oriented, imperative, and functional programming.

Let's start with a brief overview of different paradigms we'll be discussing. We'll start with the most common, object oriented (OO). This is a good default paradigm to choose in most situations and certainly the most popular one. Basically, we're going to create objects that represent objects in the real world such as bank accounts, people, and accounts to solve our problems.

The strengths of this paradigm are that it naturally leads to intuitive code if done well, with nice statements like account.withdraw(50) and user.revokeToken(). Almost every developer in the market is familiar with it, so it is easier to hire and ramp up developers on your code stack. It tends to be testable and readable, again if done right which there are entire books dedicated to. An advantage is it abstracts away how the computer actually executes things and lets us focus on our business domain problems.

This paradigm has some drawbacks as well. The code tends to be verbose and includes overhead for creating objects. It may lead to more objects being instantiated than necessary to complete your task. It can lead to tricky bugs when how garbage collection works is actually important (language dependent). It also doesn't necessarily lend itself well to high performance code, or low-level code. Firmware devices often need to allocate all their memory up front to prevent any memory issues, which doesn't fit the normal OO paradigm. For high performance code we often want to directly work closer to the memory, with control over exactly what the computer is doing which might not be achievable with OO. (Note it probably is achievable, but down to other choices like which language you write in).

Let's dive a bit more into the imperative paradigm, which is the most common for high performance or low-level code. This is a simple paradigm, we simply tell the computer what to do. We don't introduce unnecessary abstraction, and we work close to how the computer actually does things. The advantage of this is, the programmer is given great context on how their code will actually execute, what order it will execute in (possibly), and exactly what is happening with memory. This is necessary when we're talking about life-critical systems like aerospace or healthcare. It's also necessary in firmware devices with limited memory or compute where we can't afford the overhead of OO.

Another advantage is performance, as we work directly with the computer and can execute things concurrently, utilizing matrices that can be executed in massively parallel fashion on GPUs or even entire GPU clusters. The same code can often be used on CPUs as well.

The drawbacks are, the code tends to be less readable, as often there is a level of assumptions built into the code and how it interacts with the computer. It is harder to test, as tricky memory issues can arise.

Another fashionable paradigm these days is functional programming (FP). Functional programming focuses on pure functions, that is every function should have an input and an output, not directly modify state. Any necessarily impure functions such as IO, or network calls are pushed out to the edge of the program. The benefits of FP are testability, as setting up and tearing down tests is more easy with pure functions. It tends to lead to very readable code, as it encourages heavy use of functions. Pure functions should also reduce bugs as an entire class of concurrent state altering bugs is mitigated by this approach.

The drawback of this readability comes with the cost of verbosity. It also has a reputation for being less performant, with the overhead of more function calls and tail recursion for iteration. Another non-technical drawback I've observed is the lack of developers with good familiarity in FP. If you write your functions to the most general level, you're getting into the realm of abstract algebra and category theory. That is, you will be dealing with code that operates on monads and functors, which most day-to-day professional programmers are unfamiliar with. It may take more time to hire FP programmers, and for them to ramp up on your projects.

For most projects in the web development space, which is my expertise, I would recommend an OO paradigm to start. As you scale, you can write low-level, high performance services that your OO services call, and utilize caching to improve performance. This can achieve the best of both worlds, readable OO code for most of your code base, with high performant code for your most used functions. This approach has been used to serve millions of requests per second. If you utilize FP, you need to pay careful attention to how abstract your code becomes over time, and if it is reasonable with your hiring and ramp-up requirements.

Those are my thoughts on programming paradigms. Once you've chosen a paradigm the next step is to choose a language.

Enjoyed this article? Tip me on gofundme.

© 2023 Jon Crain. All Rights Reserved.