Keeping things simple – a challenge all teams face
Ahhh, the most given advice in the software development word:
“Keep it simple.”
(Next to “don’t repeat yourself“.)
The advice that is easy to give, but no one knows exactly what it means in practice. The major issues I have with this statement are:
1. What do I need to do in my day-to-day work to “keep it simple.”
2. It is subjective. You will get different answers from different people on the meaning and how they think we can “keep it simple.”
3. It can be applied to many levels, not just writing code.
Define what “keep it simple” means for you or your team
Because the nature of “keep it simple” is subjective, it is more practical to come up with a definition of what it means for you or your team. What works for one person or team, might not work for the next because the circumstances are different between the two.
This means we have to come up with an answer ourselves on how to keep things simple.
In the rest of the article I want to take a look at what “keep it simple” means for us and the decisions we made on the different levels on how to implement it.
Always start small or with less, add as you need
As a general rule of thumb, try to start with the smallest possible idea or implementation, and try to achieve some goal, adding things you really need along the way.
This might seem like an obvious idea, but it takes a bit of effort to get right because this is not our natural way of thinking. As humans, we like to make things complex and big by default, so we have to practice to take an idea or implementation and rework it to remove things we don’t really need (read: making things simple requires more effort).
For example, if you are writing a service that provides centralized authentication to other applications. Your immediate reaction is to go full async because that is what everyone says you should do, and its trendy.
If we apply our “start small/with less, add as we need it” rule-of-thumb, we do not add async code by default, we rather start with just synchronous code (which is simpler than async code) and later when we really need the advantages of async, we add it, accepting the complexity that it brings.
Separate units of a system or code units should be about one concept.
Another general rule of thumb, is to not combine multiple concepts/responsibilities into a single unit. A system component or unit of code should be about 1 concept (where practically possible).
This keeps the system and code we build simple and easy to reason about. It should be easy to understand a unit of the system if it is only about 1 concept.
A side-effect of applying this rule is that you might end up with more components in your system because we do not combine unrelated components into one, and that is ok! This rule is about a unit of the system not having multiple responsibilities, not having less components.
For example, we have written our central authentication service and it is running smoothly. A new requirement suddenly pops up: we need a way for our applications to send notifications to users. We could just add that functionality to our existing authentication service, but that means our service will have 2 responsibilities: authentication and notification.
If we apply our “a system unit should be about 1 thing” rule-of-thumb, we would rather make another service for notifications because we do not want to put 2 unrelated concepts in 1 system unit.
There is a great talk by Rich Hickey (the inventor of Clojure) called Simple Made Easy.
Armed with these 2 general rules, lets see how we apply them in practice.
Dev tools – source control
We primarily use git for our projects.
To keep things simple, we don’t use workflows like: git flow. We prefer a simple workflow of 1 main branch, mostly working on main branch, and creating feature branches only where we are working on things that might interfere with other developers if we break something.
Dev tools – programming language
Ah, a controversial (almost religious) topic 🙂
In our team, we primarily use Microsoft technologies (.NET 4.8 and .NET 7.0) and on the language front we use a mix of VB.NET and C# (mostly leaning towards only C# in the future). We won’t be getting rid of C# anytime soon because of company constraints though.
Over the years, C# has become quite a complex programming language with lots of features, and getting more and more features with each new release.
But are there simpler alternatives to C# that can still get the job done and keep up with the times? Luckily the answer is YES 🙂
The best candidate (in my opinion) if I have to switch to a simpler programming language is Clojure. It is modern, does not change as much as other programming languages, and I can use it for front-end and back-end development.
Even though we cannot use Clojure as our main programming language (yet :)), we take advantage of its simplicity by using it to build tools that support our development process and products.
Library dependencies
Often, we need a piece of functionality that we can get from a library that we can add to our project.
We had to implement recurring jobs in our product and were looking through libraries that can handle this because we do not want to re-invent the wheel. Some other teams are already using Hangfire, but it has a lot of features we don’t really need at the moment. After some searching I came upon Coravel, which seemed simpler and would do what we need.
We applied our “start small / simple” rule-of-thumb and chose to use Coravel, until we need more complicated features, forcing us to switch to Hangfire.
Code organization
We use our own software development framework called Sunshine. It prescribes a certain way of organizing the code in our system:
Area(s)
|-> Context(s)
|-> Component(s)
If you are building big line-of-business applications, it is good to have all these levels to keep things organized and focused. But what if you are building something that is smaller, not needing all those levels?
We already applied our “start small/simple” rule-of-thumb so you can have just a Context containing 1 Component:
Context |-> Component(s)
And as the system grows, we can add the other layers in as we need them.
So this “start small/simple, add more later” rule-of-thumb is also embedded in our development framework.
Design vs Development
Ok so here is a more subtle one.
In the old days, we used to just jump into coding and did the design on-the-fly while coding. Crazy times 🙂 . This looks like a great idea because our customers and managers see instant progress, BUT this has the side-effect that you make design decisions in-between low-level implementation decisions, which turns out not to be a good idea.
By applying our “something should be about one thing” rule-of-thumb, we separated these activities so we do design separate from implementation (as much as possible). This keeps the design and implementation tasks simpler.
As part of our development framework, we do a bit of upfront design before we implement things. If we change a component, we update the design first, then make the code changes.
Iterative development
In the process of finding out what the customer wants us to build/solve, we can end up with a rather gigantic list of whishes 🙂
If we apply our “start small/simple, add more later” rule-of-thumb, we can find the most important topics to work on and build a small set of features first, which we can get into production. We can then repeat this process with the remaining features and we end up with iterative development. Ta da 🙂
This keeps things simple because we are always working on smaller feature sets, making it easier to keep track of what is going on and we always end up with with a working application. If we break something, it is easier to narrow down the culprit, because we only added a small set of new features.
Conclusion
Hopefully, I have provided some food-for-thought on this whole “keep it simple” advice and gave some realistic practical examples.
I’m sure there are a lot more interesting ways to “keep it simple” than what I mentioned here. I just wanted to share how I personally apply this advice.
This is an ongoing battle for software development teams as we build ever more complex applications.