30 September 2023

To Push or Not to Push the Crosswalk Button?

A developer's tale about responsibility, mental load, design, empathetic coding and traffic lights. Buckle up.

It’s 8 a.m. in Christchurch, New Zealand. The winter air is sharp and refreshing as you make your daily commute to the office. Headphones in, music up, you're cruising through your morning routine like clockwork. Then, it appears—the intersection, and with it, the age-old question that haunts every developer on their way to work.

Do you press the crosswalk button?

It depends

Idempotency – We, software engineers, pride ourselves on logical thinking and assess the situation with the precision of a debugger. We know the crosswalk button is idempotent. Pressing it once is enough—hammering it 20 times won't summon the walking signal any faster.

Night mode? – It’s 8 a.m., not the 1–6 a.m. window when Christchurch traffic lights shift into "night mode." That means the default logic is active. Button presses matter again. Or do they?

In maintenance mode? – You glance at the crosswalk signal—it’s dark. No red, no green. Suspicious. You remember: this intersection has a history of flakiness. Last week, it glitched. Today, it looks like it's still misbehaving probably because of the road work in the area. You read the road work will continue until the 17th.

Already pressed? – Across the street, fellow pedestrians are already waiting. Based on their posture and general vibe, they’ve probably already pressed the button. You apply your finely honed developer instincts: if the system is likely to be broken and the action is idempotent, then pressing it again is redundant.

Decision – You apply your finely honed developer instincts: if the system is likely to be broken and the action is idempotent, then pressing it again is redundant.

So, you stand tall and decide not to press the button.

Validation – Just then, another commuter walks up and—without hesitation—presses the button. You watch with much anticipation. As expected, nothing happens. No light. No change. Classic inefficient junior commuter move.

Unstoppable – With the quiet confidence that only developers can pull off, we anticipate the green light like we’re watching a CI pipeline finish: 3… 2… 1…

Just like that, the crosswalk lights up green—right on cue. Boom. Nailed it. We 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?