Improving developer productivity by eliminating Structural Errors
Developer errors are bound to happen, but we can eliminate large classes of errors by deliberating designing them away. We do this by preventing incorrect states by making them impossible to build.
Recently, while debugging some code, I came across a nefarious error that I introduced. I had passed the incorrect mongo collection to a function. đ¤Śđ˝ââď¸
While the problem was easy to resolve (once found), I wanted to prevent recurrence of the problem.
Understanding the problem
The problem occurred due to a combination of a developer error (me) and structural error (also referred to as an interface error).
To prevent the error from recurring, I needed to fix the underlying root structural error. Itâs useful to note that the recurrent developer errors are often symptoms of structural errors.
Making impossible states impossible.
I formally came across the idea of âmaking impossible states impossibleâ when learning Elm. Richard Feldman had shared his insights in a conference talk calledâŚyou guess it⌠Making Impossible States Impossible.
Richard spoke a length how you can design your code to simply prevent errors from happening by being intentional - emphasis mine.
By adopting a similar approach, I realised that I could prevent future me from being able to make these errors altogether.
I want the Go code linter and compiler to help me and I shouldnât have to wait until regression testing or runtime to identify these errors.
This is immensely valuable as it helps to reduce Escaped Defects.
Refactoring away API ambiguity
In my case, the specific structural error that I was trying to resolve is parameter ambiguity. This error allows the incorrect parameters to get passed to functions.
In the example below, the col
parameter is somewhat ambiguous. The function will accept any type of mongo.Collection
without providing a compile error.
func getAllSomething(col *mongo.Collection) error {...}.
This may be ok if we have generalised all of our collections and we want the getAllSomething(âŚ)
to work on any and all collections.
However, if there is logic that is unique to some collection, then having an ambiguous col
parameter may result in future.
For instances, the three functions below all suffer from this problem.
func processAccounts(col *mongo.Collection) error {...}
func processTransactions(col *mongo.Collection) error {...}
func processLastActivity(col *mongo.Collection) error {...}
Make it more explicit
Could making the code more explicit be a viable approach to fixing this structural error? Like soâŚ
func processAccounts(acc *mongo.Collection) error {...}
func processTransactions(trx *mongo.Collection) error {...}
func processLastActivity(act *mongo.Collection) error {...}
Although it helps with linting, the code would still compile and result in Escaped Defects.
This is because we are simply renaming the ambiguous col
parameters (i.e. acc
, trx
, act
, etc.) and then relying on the developer to pass in the correct parameter to the correct function: processAccounts(acc)
vs processLastActivity(acc)
.
To eliminate the error, we need to ensure that the code wonât build when there is an error state.
There are a couple of ways to achieve this in Go.
Creating named types (partial solution)
Using composition.Â
Refactoring: Creating name types
This is easier approach of the two results in a partial solution.
We could create the following named types (which are akin to aliases).
type AccCollection *mongo.CollectionÂ
type TranCollection *mongo.Collection
type ActCollection *mongo.Collection
And use them in place of their more âgenericâ counter-parts.
func processAccounts(acc AccCollection) error {...}
func processTransactions(trx TranCollection) error {...}
func processLastActivity(act ActCollection) error {...}
With this approach, we get a linter warning and a compile-time error if we passed the wrong collection parameter to a function: processAccounts(act)Â // error
I say this is a partial solutions as weâve only moved the error further into the core of the codebase. The type alias solution still permits the possibility of errors.
For instance:
someActivitiesMongoCollection := initActivitiesCollection(...)Â
var act ActCollection = someActivitiesMongoCollection
But this will still work (which we donât want).
// This is and unintended error.đđ˝đ¤Ź
var trx TranCollection = someActivitiesMongoCollection
Refactoring: Using composition
Option 2 is my preferred way to solve ambiguity-type structural errors.
This involves the use structs
which allows me to implement a compositional approach. đĽł
type Accounts struct {
col *mongo.CollectionÂ
}
type Transaction struct {
col *mongo.CollectionÂ
}
type Activities struct {
col *mongo.CollectionÂ
}
func getAllAccounts(accounts Accounts) error {...}
func getAllTransactions(transactions Transaction) error {...}
func getLastActivity(activities Activities) error {...}
The overhead here is that youâll now need to create constructor functions if you want your code to be idiomatic.
func NewAccounts(...) *Accounts {
return &Accounts{initCollection(...)}
}
func NewTransaction(...) *Transaction {
return &Transaction{initCollection(...)}
}
func NewActivities(...) *Activities {
return &Activities{initCollection(...)}
}
Further improvements would include embedding the functions in the structs.
type Accounts struct {
col *mongo.CollectionÂ
}
type Transaction struct {
col *mongo.CollectionÂ
}
type Activities struct {
col *mongo.CollectionÂ
}
func (acc *Accounts) getAllAccounts() error {...}
func (trx *Transaction) getAllTransactions() error {...}
func (act *Activities) getLastActivity() error {...}
Team health
The bigger picture is that by reducing these types of errors in your own codebase, you may help to improve your teamsâ health.
How so�
Change Failure Rate (Number of Incidents/Number of deployments), Flow Distribution (Percentage distribution between features, defects and debts work items) all contribute to the overall quality of Team Health (quality of engagement based on distributions of work items in terms of type and quantity).
Among these metrics, code quality is a significant lever in improving not only team satisfaction and engagement but also customer and business value.
When we address structural errors, we immediately reduce the likelihood of developer error and add some levels of anti-fragility (which goes beyond resilience or robustness) in our systems.
Takeaways
Developer errors are bound to happen, but we can eliminate large classes of errors by deliberating designing them away.
When you are experiencing large cases of recurring errors, explore if the developer errors are a symptom of structural errors
Reducing structural errors goes a long way to improving developer productivity and team health.