Defensive programming is a programming discipline designed to ensure reliability of a system in case of known failure. It focuses on error handling and input validation to make sure that your application is resilient to failure.
Enterprise Craft lists several key concepts of Defensive Programming
- Test preconditions before operations
- Check for nulls
- Assert state whenever you are changing it
These are great practices to do, but I argue that there are a few things missing
- Defensive programming focuses on what happens inside of your application, and spends less time worrying about what could be happening outside your code
- Defensive programming tends to assume your program will be the only thing modifying state
- Defensive programming works best when the APIs or SDKs your working with provide you with good exceptions and errors
For those reasons I’ve started using Paranoid Defensive Programming in some scenarios. Paranoid Defensive Programming adds a couple concepts to the list above
- Assert state before, during, and after you make a change
- Verbosely log all of the result
To accomplish those goals we follow a few guidelines
- Extract a state testing function because you’ll need to repeatedly test state
- Log state results that could be benign at “info”
- Log state results that mean there is an error at “error”
- Log state results that are ambiguous at “warn”
Let’s walk through an example. Let’s say we have a function that resetUserPassword() that resets a user’s password to a random value, and another function getLastUserPasswordReset() that returns the last time a user’s password was reset.
Our goal is to reset the user’s password if it has been the same for more than 30 days. If we know that resetUserPassword() will reliably throw exceptions or return errors we can just do defensive programming like we normally would.
But here’s the catch: what if we know that resetUserPassword() is unreliable. Maybe there’s a bug in the password generator and the new password doesn’t always meet complexity requirements, maybe it depends on some interaction from the user that is hard to predict, maybe the developer didn’t take the time to throw reasonable exceptions (or throw exceptions at all), but for whatever reason we don’t trust resetUserPassword() to do it’s job correctly.
Our Paranoid Defensive Programming code should look something like this
original_reset_time = getLastUserPasswordReset("myUser")
log("Last reset time was: " + original_reset_time)
if(original_reset_time < date().days - 30) {
log("Password is old, resetting")
resetUserPassword("myUser")
new_reset_time = getLastUserPasswordReset("myUser")
if(new_reset_time < original_reset_time) {
log("password reset correctly")
} else {
log("password failed reset!")
}
} else {
log("password is new, moving on")
}
As you can see we’re bending over backwards, to look for and record errors, but this approach gives us confidence that we’ll know the state of our system when we’re done.
When is Paranoid Defensive Programming a good idea?
- When you are calling an unreliable API, and you do not trust it to return errors or exceptions
- When you do not have the ability to change the system you are calling, it might be owned by another team or outside your org
- When having consistent results is very important, but you aren’t working with something as reliable as a database (i.e. config management code)
When is Paranoid Defensive Programming not a good fit?
- When you have a reliable API that returns consistent errors
- When you have a chance to make your upstream dependencies more reliable
- When you have other frameworks that will handle idempotency for you