Capturing Intent – Making sense of code

Picture the scenario: You are staring at the computer screen and scanning the lines of code that fill it. You scratch your head and think: Why?

You debug the code, step-by-step and see a method call that sticks out like a sore thumb. It’s doing an out of process call to another subsystem before the code has completed its job in this method.

Why is that out of process call made there? There’s no documentation explaining why. Moving it to the end of the method makes more sense, but then everything breaks. WHY? 


As a software developer, you might have experienced situations like this. You’re looking to fix a bug or extend a feature and you stumble over code that just doesn’t make sense.

You can see what it’s doing, even understand how it’s works, but you just can’t understand why.

In this post I’ll show you why making the extra effort to capture the intent of the code is so important.


Defining “Intent”

For this article, I define “Capturing Intent” as “why”. Why does this code exist? What is its purpose? Which business need is it covering?


You may already be spending a lot of time writing code and shipping products. Why spend even more time trying to capture the intent?


One reason is because developers spend most of their time trying to understand code.

Jeff Atwood summarizes:

If you ask a software developer what they spend their time doing, they’ll tell you that they spend most of their time writing code.

However, if you actually observe what software developers spend their time doing, you’ll find that they spend most of their time trying to understand code

Robert C. Martin writes in Clean Code:

“Indeed, the ratio of time spent reading versus writing is well over 10 to 1. We are constantly reading old code as part of the effort to write new code. …[Therefore,] making it easy to read makes it easier to write.”

So, looking at ways to make our code more understandable seems like the right thing to do.

Breadcrumbs while bug fixing


Codebases that ooze intent are easy to navigate. This is especially useful when bug-fixing, understanding edge-cases and debugging. What this means is you have a narrative you can follow, and hints guiding you through the business logic.

This narrative serves as breadcrumbs to guide the next developer. Quite often that next developer is you!

Future proofing / Saving time

In this context, future proofing isn’t about creating code that covers future scenarios (over-engineering). It is having code that states why it exists.

Allowing a developer to understand implementation and architecture to evolving needs with minimal effort.

Show me some code!

You’ve now seen some of the benefits of writing code with intent. Now let’s look at some concrete examples and different techniques that support this.

Descriptive implementation

With “Descriptive Implementation” I mean structuring code so it’s understandable. Where it communicates its reason to exist in a clear and concise way.

Readable code

Clean code is a great starting point for writing code with intent. This means writing code with descriptive names, proper scoping and correct abstractions. Dealing with code in bite-sized chunks also aids readability. The examples are quite simple, but imagine them as a replacement.

There are a few key changes between the Unclear and WithIntent versions.

  • Easier to understand the flow of code.
  • Clearer naming.
  • Hid the conditional behind a named function
  • Focused methods that can be refactored further if needed.
  • Removed the need for comment with explicit naming of the tracking function.

The method AddCartItemAndDoTracking is still unclear, since it has a side-effect, which is tracking. One way to solve that is with a decorator:

Unit tests / specs as describing intent

Sometimes the implementation doesn’t or can’t convey enough intent. In these times it’s good to use code near the implementation to help communicate. This is one of the most useful features of writing good tests / specs.

Unit Tests / Specifications

In this example you’ll see the matching unit test / specification of the above ShoppingCart. The goal is to document the edge-case scenario of extra logging. The spec is written with Machine Specifications (MSpec).

Here’s another variant written in a more traditional NUnit approach.

High-Level Behaviours

High-level behaviour tests are another way to document intent and behaviour of a system. They serve as readable specifications which business owners can read, understand and write themselves.

The idea is to allow non-programmers to be able to define requirements up-front. It’s then up to developers to attach code that meets those requirements. A major benefit is that the specs actually describe the system in the language of the business.

These specifications are still code, so they are maintainable and better reflect the implementation. The following example is a SpecFlow spec, which describes a business requirement.

Written Documentation

Documentation is a love/hate topic when it comes to software development. Especially when there are so many (forced) requirements within a business context.

Open-source communities have a strong need for good documentation. When a community has this, it’s easier to onboard new users and support existing users.

Written documentation has the added advantage of being free-form, which leads to declarative documentation. However there’s a danger that documentation on this level will get outdated.

Function / Class Documentation

Formal class / function documentation uses formatted comments to create API documentation. This enhances the intent through auto code completion tools such as IntelliSense. You can also expose it to 3rd parties.

This may be practical for certain scenarios for library authors or shared internal tools. On a negative side it’s still free-text, which must match the implementation.

Avoid Code Comments

As a rule of thumb, code that needs inline comments to describe its intent needs improvement. There will be cases where adding a comment makes sense, but this is a last-resort approach. As with any rule, though, it depends on your context.

Here’s a good article on the topic.

Source Control Messages

The source control system is another great place to document choices made. When used right, commit messages can serve as a source of the original intent behind a change to the code. The value is seeing the context in which code has changed.

This is especially valuable when working in a long-living, legacy, codebase. Open-source projects also benefit from good commit messages since there are many different contributors.

A common pitfall to avoid is when source control is the primary source of communication.

Read this post for a better understanding on how being more result and user focused will improve your commit messages.

Issue tracker

An issue tracker is a system where you can keep track of issues for your product, and usually have an open discussion around a case. It’s where a developer may first look to understand what change is needed in the system.

These issues could be written as a User Story, but can also in many other ways. Issue trackers aren’t a replacement for direct communication. Rather they serve a common goal of documenting the change needed, and the choices involved to make that change.

Examples of issue trackers are: GitHub Issues, Jira, Team Foundation Server, Trello, Pivotal Tracker.

These systems are completely separate from the code, but it’s possible to integrate them in your workflow.

An example would be to create a branch when moving a story to in progress. Another would be to move a story to done after merging and deployed to production. You can also add the issue id in a commit message, which leaves a comment on the issue itself. This adds yet another layer of context, which can be valuable for understanding the intent of the feature.

Developer actions straight on an Issue in Jira.
Developer actions straight on an issue in Jira.

Usually the most important part of the issue is the original intent / goal. Looking at the code of an issue after it’s done isn’t always easy. Sometimes the issue will give valuable context of the decisions of the background. Other times it only contains hints of information, and doesn’t capture changes made underway.


At the highest and furthest level away from the implementation is general documentation. Usually documentation resides in a wiki-system with coding best practices, high-level architecture, descriptions of bounded contexts and repositories. This is also where some of the generated documentation may end up. This kind of documentation may serve as a good way to share original intent, but tend to stagnate over time.

Open source communities are often quite reliant on good documentation. When a community has enough contributors, the documentation often accurate, relevant and up to date. Keeping internal documentation at this level, though, is difficult at best.

Be aware of your context 

The level of intent you need to include will vary between codebases and teams. Each team will have different needs to capture and share intent.

Are you working on an Minimum Viable Product for a startup with a team of 3? Or on an internal accounting system of a Fortune 50 company with 50 others? Are you the sole contributor, on a tightly knit co-located team, or a distributed team spread across timezones?

Is it a greenfield application with the latest tooling support? Or an ageing application with an equally ageing programming language?

No matter what your context, you are responsible as a developer to convey the intent of why that code exists.

Communication and Empathy

Code is the result of communication between people. It’s this communication that leads to the need to capture intent in your code.

Sometimes this communication leads to not needing to write code at all. It also leads to a shared understanding of the problem that you’re trying to solve and how you want to solve it.


An open channel to stakeholders, developers and users is essential for understanding the actual needs for the system. Being able to empathise with the parties involved can lead to caring more about the outcome of writing the code. This in turn results in cleaner code, with better naming and a natural focus on the end result.

Final thoughts

In this article I’ve introduced you to the concept of capturing intent in your code. You’ve learned how to do so at different levels of the software development stack. With this knowledge you effectively communicate with other developers through code.

The closer you communicate your intent to the code implementation, the better. I believe this is the best way to produce maintainable, well-written and self-documenting code.

The further away you focus, the harder it is for the next developer to discover the code’s true purpose.

The ordering of the topics isn’t by accident. The more synchronous the communication, the easier it is for the intent to come across in a meaningful way. Software with intentful and clean code isn’t a replacement for healthy communication practices.

How do you communicate intent of your code with your team? Reach out to me directly if you have any thoughts, questions or criticisms. Or leave a comment below.

A special thanks to Kevin O’Shaugnessy from for his help with this article.

Don’t miss out! Subscribe to be notified of new articles that will help you deliver code with empathy. 

  • Harry McIntyre

    One technique I like to use is to intersperse the Gherkin statements through NUnit code, rather than using SpecFlow (see The specifications are still captured, and it fits into a teams existing development practices.

    I have a Roslyn script which can then extract the specifications from the .cs files later if anyone wants to read them.

    • Thanks for sharing that Harry. I really appreciate the simplicity / practicality of your approach. Certainly a low-threshold way to introduce the value of a behaviour-driven approach.

      Do you have a post or screenshots with your Roslyn script and its outputs?

      Personally, I find value in having a different “feel” and syntax to my specs vs implementation because it forces me into a different mindset. I can recognise that there are many needs, preferences and team dynamics to take into consideration.

      PS: the link to your blogpost is broken, a quick edit should fix it. Thanks

      • Harry McIntyre

        My feeling is that separating the specs and implementation tests into different projects isn’t necessary. Sometimes I implement a spec using a webdriver test and full environment, and sometimes using an isolated unit test, (see testing pyramid) but each one still corresponds to a specification, and there’s less to think about if I don’t have a hard separation.

        I have found that if you follow the approach of inline Gherkin, you can easily see after doing TDD which tests are regression tests (the ones with inline gherkin specs) and which tests are ‘throwaway’ tests, which only exist due to the specific implementation. This is probably a form of ATDD (acceptance-TDD).

        • Thanks for sharing your approach, Harry.
          If I understand you correctly, you are using a grey-box testing approach, using black-box testing to allow for flexibility of the implementation, and white-box / TDD for parts needed. I’ve heard about the testing triangle, but don’t know details.

          I’m curious about the “throwing away” of TDD tests. I’m not familiar of the concept so, need to look more into that. Based on the assumption that some tests that are created during TDD only have value to drive the implementation, I can see that knowing which tests own the specification would help.

          Food for thought, and things to look up and learn more about. Thanks!

          • Harry McIntyre

            Thanks for the thoughtful response!

            Throwaway tests are ones which are coupled to the implementation, not the functionality – i.e. if you were to rewrite the feature in a different way they would no longer be needed. Traditional TDD causes both kinds of test to be created in the same project.

            As a contractor, I change company frequently (it’s like always being a new hire, so you spend a lot of time learning new code bases and organisational structures). So much time is spent doing ‘archaeology’, finding out why a particular piece of code was written the way it was, and if it was/still is important. Having specs alongside the code makes this much easier.

      • Harry McIntyre

        There’s a LinqPad script here, with an early version of the script:

      • Harry McIntyre

        You might also be interested to see which I spotted earlier today.

  • Michael Smith

    Great post Pav!

    I like your summary: “The closer you communicate your intent to the code implementation, the better.” As values go, it’s a good one. 🙂

    It’s one of the reasons I’m such a fan of Commands and Events. Correctly done, they are the very essence of intent. I would tend to use a decorator for more “technical concerns” rather than something that is Domain related. Adding an item to your shopping cart is an important action in your domain and deserves to be modelled as such. It’s easy to imagine a lot of other things you might want to do in response to this event – update recommendations, recalculate tax and freight, distinguish “dead” from “alive” carts, etc, etc. Custom services and event subscriptions can keep this decoupled and really help in expressing intent.

    However, capturing this intent and keeping things decoupled (whether through Events or through a Decorator) comes at a cost, which we have to recognise. It’s harder to just dive into the code and know what will happen straight away. The indirection means you need to understand the architectural flavour of the app before jumping into it. Senior devs, who have been around the block a bit, can probably recognise styles quickly, but inexperienced and junior devs are not going to just from reading the code, if they don’t already know them already. If you are using Events, you are going to have know to look for the Subscribers to an Event and understand the semantics of events. If you are using decorators, you are going to have to know the pattern and see where it is configured (e.g an IOC, Configuration Root) to understand how the pipeline is built up.

    I think you are touching on this with your discussion of the Wiki and the importance of context (another very good general rule!). I would just want to be explicit and say that the idea of capturing intent in your code doesn’t equate to a new dev being able to open up your code and immediately read it, understand it and be able to change it. That is just a conceit of developers. The idea is that the code captures the current understanding of the domain. As a dev, you aren’t going to be able to jump from zero to a full understanding of the domain and architecture just by reading the code. You need to engage with your domain experts, fellow devs, testers and users.

    I don’t think it’s in any way inconsistent to say that we should always strive to make our code reflect more of the “why” in preference to the “how and what” while at the same time recognising that you will never really understand the “why” (or the how or what for that matter) if you only engage with the code. Just because the code should be the most complete representation of our understanding, doesn’t mean that it is necessarily the best starting point. Again, I think you capture this with your section on Communication and Empathy.

    I think I might break the DRY principle and again say “Great Post Pav!” 🙂

    • Thank you for your wonderful reflections, Michael 🙏. It’s worth a blog-post by itself. These parts really stood out though:

      It’s one of the reasons I’m such a fan of Commands and Events. Correctly done, they are the very essence of intent.

      Senior devs, who have been around the block a bit, can probably recognise styles quickly, but inexperienced and junior devs are not going to just from reading the code, if they don’t already know them already.

      I would just want to be explicit and say that the idea of capturing intent in your code doesn’t equate to a new dev being able to open up your code and immediately read it, understand it and be able to change it. That is just a conceit of developers. The idea is that the code captures the current understanding of the domain.

  • Tomas Ekeli

    Damn, this was a great one, Pav!