30 September 2023

To Push or Not to Push the Crosswalk Button?

A story about responsibility, mental load, design, empathetic coding and traffic lights. Buckle up.

Original opening narrated by ChatGPT for dramatic effect.

It's 8 a.m. in Christchurch, New Zealand. The air is crisp, carrying the invigorating chill of winter. You're on your daily pilgrimage to the office, striding purposefully with headphones snugly in place, drowning out the world with your favourite tunes. As you approach an intersection, a familiar conundrum beckons - the eternal question that plagues the minds of all software engineers.

"Do you press the crosswalk button?"

Ah, but for us, the elite of the coding world, the answer is a resounding "it depends…"

It depends

As software engineers, we exude competence, and know that hammering the crosswalk button is about as impactful as shouting "Hello world" into the void. The action, you see, is idempotent. So, we logically dismiss the notion of frantically jabbing at it multiple times. Thus, we are left with two clear-cut options: to press or not to press.

But hold on, before you make your choice, let's dissect the situation further. You see, traffic lights in Christchurch pull a little trick between 1 a.m. and 6 a.m. to ease the night's burdens. But, it's not the dead of night when traffic lights don their nocturnal persona. It's 8 a.m. and the default crosswalk behaviour reigns supreme.

Ever observant, we notice that the crosswalk signal is unlit as if taunting our decision-making skills. As astute engineers, we know this particular traffic light has been struggling with maintenance issues. Alas, the red light remains dormant, and only the ever-reliable green light is doing its job.

And then, the pièce de résistance! We glance across the street and spot fellow pedestrians. They, too, seem to have engaged in the ritualistic button pressing while the crosswalk signal remains stubbornly unlit. Ah, the odds are in our favour. As seasoned software engineers, we confidently conclude there's no need to push the button.

Just as we revel in our decision, another commuter joins our side. In a decisive move, they push the button. As we anticipated, the crosswalk signal remains indifferent, displaying no hint of crimson. Aha! We knew it all along – the light is, in fact, malfunctioning. Oh, the folly of our fellow traveller!

With the self-assurance that only software engineers possess, we sense the impending green light. It's a countdown we're all too familiar with – 1… 2… 3! And like a scripted sequence in our meticulously coded program, the crosswalk instantly bathes in green. Boom! We, the unsung heroes, have once again saved the day. The answer was crystal clear: pushing that button was unnecessary.

With a triumphant smile, we step onto the crosswalk when a hand suddenly grabs our arm and pulls us back onto the pavement as a car speeds through just in front of us.

It depends on the wrong reasons

Here are code drafts of our logic and our life saver, respectively

class Developer < Person
  include UrbanWalker

  def cross_the_street
    cross_light_button.press if should_press_button?

    until cross_light.green? do
      cross(road)
      break
    end
  end

  private

  def should_press_button?
    # Our worldwide understanding of traffic light rules
    TrafficLightButtonRules.should_press_walk_button?(self, time: Time.current)
  end
end
class LifeSaver < Person
  include UrbanWalker

  def cross_the_street
    cross_light_button.press

    until cross_light.green? && road.safe?(traffic) do
      cross(road)
      break
    end
  end
end

We can see that we definitely know too much about how the traffic light works.

Responsibility & Premature Optimisation

We've added much code to optimise our procedure from unnecessary tasks, like pressing the button. The #should_press_button? method could be a simple if/else statement instead of the current TrafficLightButtonRules module; the question would remain the same.

Is it our responsibility to know the button's status and how it works?

Even if pushing the button resulted in an expensive or time-consuming outcome, who is more likely to understand when to perform that costly action: you or the traffic light?

Advice #1: Let the code run

Maintenance & Mental Load

Code procedures aren't subject to forgetfulness, but developers writing the code are. We spent a lot of unnecessary time developing and maintaining TrafficLightButtonRules, which can be distracting. We've now inherited new code to maintain. We have to dedicate some of our attention to these rules. The brain must ensure the button rules are up-to-date and well-tested. And in the story, we increased our chance of focusing on the wrong thing and endangered our life.

Removing these pressing rules and applying the same behaviour when facing the button is liberating. The person who systematically presses the button gets more attention span to focus on more critical aspects of road crossing.

Advice #2: Seek less logic

Design & Trust

The traffic light is well designed and can handle multiple presses, considering we TRUST the traffic light to function correctly. Imagine a traffic light with the following features:

  1. It has a button that, when pressed, triggers a 30-second countdown before stopping the traffic and allowing people to cross safely.
  2. Each time the button is pressed, it resets the countdown timer.
  3. The traffic light does not display the countdown timer.

Here is an incentive to know every traffic light's behaviour and button status. People may need to develop strategies to tell whether someone has already pressed the button. This example demonstrates terrible design and would be a pain to use.

Knowing too much about other objects for the code to function correctly smells like of bad design. The current design of traffic lights is excellent. We don't need to know whether to press it; we can push it every time.

Advice #3: Design with trust in mind

Yet it still depends

Let's pause for a minute and think about reasons not to press the traffic light button:

We could group these reasons into two categories: I can't or don't want to push the button. These reasons show it doesn't depend on the walk button status at all but on our capability to press it.

It depends on my status, my ability to push the button and not on whether the button should be pushed. Often I can do it.

Advice #4: Execution context should mind its own business

Examples

Knowing too much

Have you seen this type of code before?

class Post < ActiveRecord::Base
  def publish
    update(published: true) unless published
  end
end

What was the intent here?

We thought calling #update would result in some side effects like changing updated_at even though published is already true. Maybe we were scared the database would get an unnecessary hit. We do not trust the code to behave appropriately.

But, just like the walk button, #update is well designed. You don't need to know the status of post to update it, you do. ActiveRecord won't apply the update when no attribute has changed. Nothing really stops us from executing the code. Therefore we should call the update method.

This method doesn't prevent the execution of code, has less logic and assumes trust in #update.

class Post < ActiveRecord::Base
  def publish
    update(published: true)
  end
end

Minding your own business

The next code blocks are inspired from an AppSignal article: Three Ways To Avoid Duplicate Sidekiq Jobs. The difference is that I'm using ActiveJob instead of Sidekiq.

In this example, we want to prevent a job from being scheduled multiple times. One option suggested is to have two flags that BookSalesService can query.

Disclaimer: I don't know whether the following code blocks work. Take it as pseudo code more than anything.

# 1. DIY section
module BookSalesService
  def schedule_with_two_flags(book)
    # Check if sales are being calculated right now
    return unless book.sales_enqueued_at > book.sales_calculated_at

    book.update(sales_enqueued_at: Time.current)
    BookSalesWorker.perform_later(book.id)
  end
end

class BookSalesWorker < ActiveJob::Base

  def perform(book_id)
    crunch_some_numbers(book_id)
    upload_to_s3
    # New adition
    book.update(sales_calculated_at: Time.current)
  end

  # ...
end

The intent is sound yet the implementation can be improved. The code is written so that BookSalesService wonders whether it should queue the job instead of queuing the job.

Here is what it could look like if the service was minding its own business and trust the worker from doing what's right.

module BookSalesService
  def schedule_with_two_flags(book)
    BookSalesWorker.perform_later(book.id)
  end
end

class BookSalesWorker  < ActiveJob::Base
  around_enqueue do |job, block|
    book = Book.find(job.arguments.first)
    if book.sales_enqueued_at < book.sales_calculated_at
      book.update(sales_enqueued_at: Time.current)
      block.call
      book.update(sales_calculated_at: Time.current)
    end
  end

  def perform(book_id)
    crunch_some_numbers(book_id)
    upload_to_s3
  end
end

This is an improvement, as only the worker needs to know about the two flags on books table now. The around_enqueue block makes the intent clearer, and the #perform action is free from any confusing and unrelated logic. Can it be improved further? Probably.

Conclusion

This post is part of a bigger idea I'm trying to validate and crystallise around business logic and responsibility. Anyway…

Fun facts about traffic lights

There is no definitive answer to the question "Does pushing the crosswalk button actually do anything?". Depending on the location, the time, the type of crossing, it will behave differently, but statistically it seems that most crosswalks are automated in big cities. Here are some fun facts about traffic lights:

Are you Key or Peele?