30 September 2023
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?
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.
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.
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
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
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:
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
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
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
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.
BookSalesService
queued BookSalesWorker
trusting it from performing the job twice?BookSalesService
's responsibility to control the worker's queuing?BookSalesWorker
somewhere else?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.
This post is part of a bigger idea I'm trying to validate and crystallise around business logic and responsibility. Anyway…
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:
The world's first traffic light was installed in London in 1868, but it was not automatic. Instead, it was manually operated by a police officer.
Traffic lights weren't always three-coloured. The Invention of the Three-Light System was by a Detroit police officer named and William Potts and later patented by Garrett Morgan in 1923 ref, ref
Japan has blue traffic lights in some areas because historically the Japanese used the same word for green and blue.
Red and yellow cards in FIFA were introduced after a referee was stopped at a traffic light and thought "Yellow, take it easy; red: stop, you're off". They also helped to address the communication issues due to the various languages involved. ref
The only upside down traffic light in the US is located in Tipperary Hill, Syracuse, New York video
The narrowest street in Prague has its own traffic light for pedestrians. video