Programming is cognitive work, and we as programmers perform best under intense concentration. While there are external factors that can affect this, such as having a quiet office, controllable ways of communication, etc., there are also some internal factors that need to be taken into account. In fact, the way we work can influence the quality of the outcome the most.
The state in which we are 100% focused while pure magic is coming out of our keyboards is often referred to as the zone, or flow.
Flow is not easy to get into. One interruption is enough to blow it, and, once lost, it's usually difficult to regain. It usually takes me half an hour to get back in the zone with the same level of intensity.
Flow is also a crucial ingredient to getting meaningful creative work done. In Creativity: Flow and the Psychology of Discovery and Invention, psychologist Mihaly Csikszentmihalyi identified nine elements that can get anyone into a state of flow:
- There are clear goals every step of the way.
- There is immediate feedback to one’s actions.
- There is a balance between challenges and skills.
- Action and awareness are merged.
- Distractions are excluded from consciousness.
- There is no worry of failure.
- Self-consciousness disappears.
- The sense of time becomes distorted.
- The activity becomes autotelic.
Test- or behavior-driven development (TDD/BDD), continuous integration and continuous delivery (CI/CD) can help programmers amplify several of these elements. In this post, I'll try to explain how.
Before I started practicing TDD and BDD, I often felt at a loss when facing a big new feature to develop. It’s not that I couldn’t do it, it's just that the way I worked would often be inefficient: I'd procrastinate while trying to decide where to start, and once I'd reach the middle, I'd often meander between different parts of the incomplete system. After the feature was shipped, much too often I’d realize that I had over-engineered the thing, while also missing to implement at least one crucial part of it.
In BDD, you always start by writing a high-level user scenario of a feature that doesn’t yet exist. It usually isn’t immediately obvious what that feature is, so taking time to think this through and discuss it with your team, product manager or client before you write any code is time well spent. Once you have the outlined feature in front of you (written in a DSL such as Gherkin, for example), you'll have a clear goal towards which you can work. Your next sole objective should be to implement that test scenario. You'll know that you've succeeded in achieving this objective once the test scenario starts passing.
As we dive into the lower levels of implementation — for web and mobile apps this is usually the view, then controller, model, possibly a deeper business logic layer — we should always keep defining our next goal by writing a test case for it. Each goal is derived from the implementation we've just completed while keeping in mind the high-level goal represented by the initial scenario. We can re-run the high-level scenario (sometimes also called acceptance test) whenever we need a hint on how to proceed.
For a more hands-on tour of how to do this in practice, check out my previous post:
Fast feedback loops are essential in everything we do if we want to be good at it. Feedback tells us how we're doing, as well as where we are relative to our goal. When debugging an issue that involves network roundtrips and multiple files, and spans a potentially wide area of code, your primary goal should be to shorten the loop of reproducing it as much as possible. This will help you spend as little time as possible typing and/or clicking in between attempts to reproduce the issue in order to see if you've resolved it.
BDD and continuous integration are all about feedback. When programming, the tests you’re writing provide you with feedback on your ongoing work. You write a test, program a little, run the test and observe the result, go back to refactor a little if it’s green, or work some more on making it pass in case it’s red.
Making this process fast is crucial, as you can lose focus if you need to wait for a single test result for longer than a few seconds. It's also helpful to practice switching between tests and code, as well as running the related test(s) part of your muscle memory almost subconsciously. For example, at Semaphore we all program in Vim and use a small plugin to run the test or test file under the cursor with a handy shortcut.
One of the biggest benefits of continuous deployment (CD) is that it enables rapid feedback for the entire company. Releasing on a daily basis ensures that developers can fine-tune the implementation, product managers can validate solutions and get feedback from users, and the business as a whole can innovate, learn, and course-correct quickly.
For a discussion on what's fast enough CI, see:
If there’s a fundamental mismatch between a task at hand and our skills, no process can help with that issue. However, TDD plays an important role as an aid in the general process of breaking up a big task into many smaller, manageable ones. It helps us work in small increments. Assuming we're working in a familiar domain, there might be one high-level scenario that's far from complete, but there are always at least some tests which we can get to pass in about an hour of programming.
In a way, CI and CD help make conquering big challenges easier too. While our final goal may be an important feature which requires multiple weeks of teamwork, continuous delivery — complete with feature flags, gradual rollout, and incremental product development informed by data and feedback — helps us ensure that our units of work are always of manageable size. This also means that in case it turns out that what we are building is not useful, we can discard it soon enough without doing pointless work.
Worry of failure often stems from the social setting. However, there's a tremendous benefit of having a comprehensive test suite that acts as a safety net for the entire team. In the process of continuous integration, we minimize the risk that a fatal bug will be left unnoticed and deployed to production by automating the process of running tests, along with performing coding style, security and other checks. This helps developers work without unnecessary stress. Of course, this is assuming that every contributor follows the basic rule of not submitting a pull request without adequate test coverage.
If your team is practicing peer code review (which I highly recommend!), then having a build status indicator right in the pull request, as provided by for example Semaphore, helps the reviewer know that she can focus on more useful things than whether the code will work.
At the end of the day, let's keep in mind that the primary benefit of working in a state of flow is our deeply personal sense of achievement and self-worth that comes with it. That's the ultimate measure of value of any process or tool.
Original version of this post was published on Semaphore blog. Happy to share on dev.to — please send your feedback in comments. ✌️