Hey! I was writing this up as a potential future project, and it occurred to me that this is a great time to share it to the eng team. It’s heavily related to some problems we are running into with migrating to our new payments infrastructure.
If you have anything to add, please add it as a comment, because I am suggesting this as a real project to be undertaken sometime in the future.
Project Idea: Reimagining the Task Lifecycle
One of the absolute foundational aspects of the TaskRabbit system is the progression of a Task from its creation to its completion by a Tasker. Along the way we create a bunch of different entities in the database. Those entities are often used to signify the progress of the Task through its Lifecycle. Things like:
assignments
appointments
invitations
invoices
(If you know anymore post them in the comments)
Making lots of different entities along the way is normal and expected. But, the problem is that we use those entities to represent the stage of the lifecycle that the task is currently in. And that, is absolutely a problem.
For example, there are many places in the code where we check to see if one (or more) of those entities exist in order to make a decision about what the business logic should do.
module Admin
module AtRiskJobs
class AppointmentRiskEvaluatorOp < Backend::Op
# … bunch of stuff omitted here
def needed?
Admin::Invoice.where(job_id: job_id).none? && appointment.state != 'canceled'
end
end
end
end
That logic’s saying
If this job has no invoices and is not cancelled, then we need to run this Appointment Risk Evaluator
You can see how that creates a very brittle piece of code when you consider a project that moves invoices into a different database table and changes the semantics of what an invoice means.
A better way to write that code MIGHT look like
module Admin
module AtRiskJobs
class AppointmentRiskEvaluatorOp < Backend::Op
# bunch of stuff omitted here
def needed?
TaskLifecycle::TaskStateGateway.fetch(job_id).appointments_risk?
end
end
end
end
But then, MOST IMPORTANTLY, we DON’T just move the Invoice query into TaskLifecycle. Instead, we do something like this (pseudo code)
# these are event handlers
when :task_is_created do |task|
set_lifecycle_state(task, :appointments_risk, true) #because a task is at risk until it either has an invoice or is cancelled, we default this to true
end
when :task_has_been_cancelled do |task|
set_lifecycle_state(task, :appointments_risk, false)
end
when :task_has_been_invoiced do |task, invoice|
set_lifecycle_state(task, :appointments_risk, false)
end
Then, when we make a big change like replacing the payments system, all that we have to do is add more when
listeners in TaskLifecycle that react to events from the new payments system, and we can trust that no other code in the system can be broken.
The reason that this works is that it turns the interface for the TaskLifecycle from a computed value
into a proper database-backed entity
. And when we’re talking about the absolute core of our system, I’m suggesting that we usually will want to do that.
If you’re coming from standard startup-world style Ruby on Rails (like me :raise_hand:) that can read like absolute overkill. But I’m suggesting to you that it’s not overkill at all. In fact, when the ex-java devs from big enterprises and giant-scale tech companies look at a more early stage startup flavored Ruby on Rails design, they are often thinking, “Whoa this is missing a lot of important design elements”
This is because as an organization scales, the benefits of this style of code begins to enormously outweigh the extra time it takes to build it that way. Because when you do something like replace your payments infrastructure, it doesn’t leave the organization faced with an essentially impossible task of finding every place in the code that relied on on internal (non-interface) level entity like an invoice.
I’m happy to discuss this in detail and argue about the specifics of my hypothetical implementation, but I hope most of you can see that the higher level concept is a sane one and something we need to aspire to.