As developers, it's easy to get lost at sea with all the patterns, techniques, tools, services, and paradigms we're expected to somehow know.
This is especially difficult when needing to design software systems from scratch. Which language should I use? What patterns should I follow? Which dependencies should I rely on? The list of questions never ends, and it can be paralyzing to think about how a perceived wrong decision from the start could come back to haunt you later.
I've experienced these fears at one time or another for most of my career, and it's really only been recently that I've adopted a change in perspective that has sincerely helped reshape my attitude and approach to software design.
This idea, distilled to its most basic foundation is pragmatism.
At its heart, programming and development is about solving problems. I've quickly discovered that the fundamental characteristics of these problems are almost always the same, regardless of the original scale or scope of the project.
For a business, the most important thing is presenting a solution to your users. In the real world, that solution doesn't need to be perfect, and it likely never will be.
We're both fortunate and unfortunate as programmers that there are often many different ways to resolve a particular problem. Being pragmatic means picking a solution and moving forward with it, without worrying about whether that solution will be viable or successful forever.
At the end of the day, it's the solution that matters. Your users and your stakeholders are very rarely going to care if you've reached the optimal resource usage or if you've solved for Big O Notation.
Now, that isn't to say that efficiency isn't important, and being pragmatic is not a blank-cheque to write sloppy code.
But it is important to keep in mind how our solutions are actually going to run in the wild. For most of us, the efficiency ceiling of our solutions will be defined by our product and not by how our users actually use that product.
When we're designing software, we should be planning at a higher level for how our solution might grow, and to ultimately make it as flexible as possible. At the end of the day, our biggest issue shouldn't be whether our solution will live on Apache, nginx, nodejs, or maybe something else -- it should be how it might be adaptable to work on any of them later.
Good software design requires maintaining a balance between reasonable planning and actually getting things done. A more pragmatic and flexible approach not only helps keep us from being boxed into corners with specific toolings, but also makes us more Agile.
Thus, instead of worrying about granular-level problems like hosting, caching, etc, we take a step back and plan from a higher level. For me, that means breaking down each of my solutions in a way that helps them meet the following core objectives, or "pillars":
It goes without saying that for most of our careers we will be spent working on software someone else has written. The number of unique opportunities each of us will have to design a solution from nothing will be few and far between.
This is why designing that solution in a way that is maintainable is so important. Our goal here is to make it as simple as possible for developers to understand our code. There a number of easy and simple ways to do this that many of you will likely already do:
- descriptive and useful inline documentation
- consistent application of style guidelines and linting
- common-sense naming rules for variables, methods, and classes
The next step is to actually look at the code we write, and how it could be improved to make it naturally easier to understand. Avoiding the "Code Golf" trap is definitely valuable and contributes a lot to making our code easier to read.
And once we've mastered these simple things to look out for, we can dive into some more complex ones like better managing our application's Cyclomatic Complexity so that it's easier to follow our execution path from the code alone.
How our solution scales should always be on our mind. How does our solution hold up with 1000 users? What about 100000? These are the kinds of questions we should be asking ourselves as we design our products.
The great thing about thinking about these questions early is that it means we can plan for how we might solve these problems later, without actually needing to implement things immediately.
The more important of these internal conversations is not what tool we will use to implement our scaling, but instead is and understanding where our potential scaling vulnerabilities are.
Understanding which aspects of your application scale vertically vs horizontally, or which processes will benefit from caching, is significantly more valuable that the implementation of these steps.
Building a stable solution is always a goal for a developer. However, in my experience, the most stable solutions are the ones that are proactive rather than reactive.
Stability can often be achieved as a byproduct of already building maintainable and scalable solution. And this makes sense since stability is really just a function of how long it takes to fix a problem (maintainability), and how well it handles traffic (scalability) over time.
The most successful solutions that we hear about in recent years have been those that tend to be developer-friendly. These huge enterprises often have dedicated developer resources where they invite others to build custom solutions for the problems their users face.
This is the difference between being a product, and being a platform.
And while not every solution, app, or service will have need to mobilize a network of developers to extend their platform, it should always be considered as a way to broaden your solution's reach.
Common and easy ways to promote extensibility include:
- making either your entire or a portion of your API public
- implementing webhooks for specific important lifecycle events
- authenticating through OAuth
Whenever possible, at least some level of extensibility should be built into your solution in order to give you room to organically grow with your users. It will never be possible to fully anticipate every need of every user -- leaving room for external developers to work with you to solve some of those unique problems allows you to focus on larger-scale goals while still responding quickly to the ever-changing requirements of your users.
Many otherwise good (or even great) ideas have died on the page because they were never given the opportunity to live. Getting stuck on the minutia of things like "should I use a factory pattern?", or "will this be deployed on Kubernetes/containers?" can often derail our progress.
If we adopt a more pragmatic approach that focuses more on the solution itself, and not the pieces underneath. Instead, we take a much higher-level view of the goals we must accomplish in order to design an effective solution. In the end, it must be:
Once we've addressed these goals, we can look to fill in the blanks as we go, rather than front-loading all that responsibility at the start.
Inherently, this mindset should allow us to develop more flexible software, giving us the freedom to make adjustments quickly and efficiently to pivot as needed to keep our solution in scope of our pillars.
I'd love to hear from you! Have you ever let a personal project die because you got overwhelmed by having to design the perfect piece of software the first time? What strategies do you use when trying to solve this problem?
Let me know!
image courtesy of Unsplash