Coding backward

Coding backward means working backward applied to coding — we start from the goal (what’s known) and work backward (the unknown).

Luís Soares
CodeX
7 min readApr 3, 2024

--

Photo by Wim Arys on Unsplash

Coding backward (or reverse programming) makes us think about intent before implementation, making the code more expressive and leaner. We begin with the end in mind. Let’s see some examples.

Begin with the test

Test-driven development (TDD) is the perfect example of beginning with the end in mind and moving toward it. It tells you to start with the test (specification) — the reason for the change and the goal — and then jump to the implementation. You do it iteratively, relying on the TDD cycle.

When we write a test, we imagine the perfect interface for our operation. We are telling ourselves a story about how the operation will look from the outside. Test-Driven Development

Beware, TDD doesn’t mean “writing the tests first”; it means writing a tiny iteration towards the goal in a test (i.e., the most straightforward assertion to have a failing test) and making it pass; repeat it till you’re happy.

TDD is interface-first, separating interface (what) and implementation (how) design decisions. It forces you to think about what before how so you’ll hardly produce cumbersome APIs; since the tests are the first users of the code, you get immediate feedback. The executable tests (safety) and tests as documentation (specification by examples) are just byproducts of TDD.

Begin with the interface

When starting the work on a user story (which is only a problem statement), it’s common to begin discussing algorithms or even the database, but that’s plain wrong. Generically speaking, you should start at the top (the top is usually a UI), build the following need, and do it recursively till you reach the lower level — top-down approach. Unlike the bottom-up approach, where you build low-level components first and try to guess what’s needed above, in the top-down approach, you’re driven by needs rather than guesses. You do solely what’s needed.

In the top-down approach, we repeat a cycle of defining and implementing an interface (interfaces can be GUIs, CLIs, REST APIs, function/method signatures, etc.). At each level of abstraction, the interface establishes a contract you must fulfill. Only after implementing it can you move further down the tech stack. For example, in web development (with client-side rendering), design a minimal UI first, try to implement it, and only then think about the needed backend API and use it (repeat this cycle until the database). If you’re API-first, you may want to start with the OpenAPI specifications.

A top-down approach ensures you don’t waste time building generic overengineered components trying to guess how they’ll be used up the stack. When you journey from the user problem to the database, you build exactly what’s needed and no more, level by level. Another benefit is that it encourages the use of ubiquitous language in the codebases since concrete needs drive the code building.

Begin with the commit message

Coding backward can be applied to commit messages. This involves preparing a clear message that states the intended outcome before making any changes to the code. Doing so can help us stay focused and avoid diversions or trying to achieve too much simultaneously. It’s especially useful for people who get distracted easily. Writing it before also helps to make commit messages less about the code and more about the added value in business terms.

A small commit is one with minimal scope; it does one “thing.” […] A commit is atomic when it is a stable, independent unit of change. Write Better Commits, Build Better Projects

Begin with the function steps

If you want to write a function, you should first write all the steps necessary to achieve the function goal without knowing how they’ll be implemented (those steps are merely calls to non-existent functions with the inputs and outputs in place). With this, you think about what (known) followed by the how (unknown). You’re driven upfront by the intended usage rather than implementation details. You correctly name a variable before knowing how it will be computed/used rather than the opposite. You call functions that do not exist (and later ask the editor to generate them). In a nutshell, you define purpose first, also known as wishful thinking programming. Naming doesn’t have to be hard if we don’t start with the tech in mind.

With wishful thinking, you write your ideal code upfront. If you’re test-driven, you start to play with a test. Don’t worry about it if it doesn’t compile or won’t run. Experiment with different variants until the code is as expressive as possible. Then you just have to make it compile by implementing your abstractions. This is often straightforward once you’ve come up with clear roles for your objects and functions. Your Code as a Crime Scene

// the function below was writen without any of the referenced functions
// (when done, you can ask your code editor to generate the needed functions)
func main() {
customers := readCustomersCSV("input_data.csv")
custumers30YearsOld := filterCustomers(customers, "age", 30)
customersWithDiscount := activateDiscount(custumers30YearsOld)
writeCustomersCSV("output_result.csv", customersWithDiscount)
}
// you should apply this mindset to any function (not just main)

Notice that the steps in the example are written almost in plain language. When you read it at a glance, it works like an outline or an index. As in a recipe, you can tell the whole story from there (besides, all the steps present the same level of abstraction). It’s self-documenting code.

Steps-first is a way to tackle inherent complexity because, on the one hand, you’re splitting a problem into smaller ones (“dividing to conquer”) — the functions you’ll create later. On the other hand, you’re forcing yourself to define ideal function signatures (and purpose) before implementation. Then, you apply this reasoning recursively, starting with the entry point (e.g., main), creating functions (at the same level of abstraction), and doing the same in each of them until the problem is solved. In such a top-down approach, you only do what’s needed.

We can use systems modeling to complement the steps-first technique: activity diagrams, flowcharts, state diagrams, sequence diagrams, pseudocode… All of this helps us before jumping into the actual code.

Begin with the comments

In a comments-first approach, we write a plan as comments in plain language to explain what we aim to do before the hands-on code. We’re forced to structure and articulate our thoughts by expressing intentions upfront. Writing the code becomes more straightforward, and we may save some back-and-forths and code rewrites.

def main():
# 1: request the type of shape
# 2: request the dimensions of the shape
# 3: calculate the area of the shape
# 4: print the calculated area
...

The second, and most important, benefit of writing the comments at the beginning is that it improves the system design. […] The simpler the comments, the better I feel about my design. A Philosophy of Software Design

Comments-first may be helpful to clarify thoughts (e.g., for a complicated algorithm) as a precursor to steps-first. However, more often than not, TDD solves the same problems.

Begin with the function goal

Beginning with the function goal means writing functions where you start coding a function by its goal (its return or side effect). The function goal is what’s known from the start. You start in the last line of code and work your way up (into the unknown). Per line of code, you must ask, “What do I need just above to make this feasible?”. Do this until there’s nothing else to do.

// Iteration 1️⃣ 
func main() {
// step 1: what do I want to achieve?
// to write a CSV of 30yo customers with discount
// now I need to compute that variable...
writeCustomersCSV("output_result.csv", customersWithDiscount)
}

// Iteration 2️⃣
func main() {
// step 2: now I need custumers30YearsOld...
customersWithDiscount := activateDiscount(custumers30YearsOld)
writeCustomersCSV("output_result.csv", customersWithDiscount)
}

// Iteration N
// ... (till you're done)

You can apply this technique to any function, especially higher-level ones.

What do you want to achieve in a function return? Start there and move your way up.

Coding backward generates leaner code because at each step, you do solely what needs to be done and no more. Each line of code is driven by its intent.

Begin with the assertion

A notable example of starting a function by its end is when writing a test, you begin with the assertion and move your way up. This means doing the Arrange/Act/Assert (the same as Given/When/Then) in the reverse order. This forces you to have the minimum code possible in that test. A test case should isolate a particular scenario, and starting with the assertion — which represents the goal and the reason for the test — is the best way to ensure it. This approach is more meaningful when doing TDD because you start exactly with your goal, having written no other code.

Iteration 1️⃣
def test_calculate_rectangle_area():
assert rectangle_area == 12
# there's an error because rectangle_area in not defined

Iteration 2️⃣
def test_calculate_rectangle_area():
rectangle_area = calculate_area('rectangle', 4, 3)

assert rectangle_area == 12

Iteration 3️⃣
# auto-generate a dummy calculate_area function
# and see the test fail


Iteration 4️⃣
# (we don't need an Arrange in this test)
# implement calculate_area solely to pass this test

Write the outputs, the assertions and the checks first. Then try to explain how to get to those outputs. […] When tests are written from the outputs towards the inputs and contextual information, people tend to leave out all the incidental detail. Fifty Quick Ideas To Improve Your Tests

Learn more

--

--

Luís Soares
CodeX

I write about automated testing, Lean, TDD, CI/CD, trunk-based dev., user-centric dev, domain-centric arch, coding good practices,