Just Write English
In my last post, I argued that business logic belongs in a model, not a job. What does that look like exactly?
In figuring out how to write a new feature, I tend to think about three things:
- Nouns
- Responsibilities
- Messages
Then, I tie them together in the most natural and straightforward way my programming language allows. The fewer words or concepts, the better.
Fortunately, in the case of Ruby, that often means simply writing English. I’ll show you what I mean
as we implement Publication::NotificationJob
.
Figuring out the interface
Finding the right nouns isn’t always straightforward. But here, it’s easy: we’re dealing with Posts
, Subscribers
, and Notifications
. Nouns—check ✅.
Next up is responsibilities. When it comes to publications, who should decide who gets notifications and ultimately queue them up? I would say it’s the publication.
If it’s not immediately clear why, consider how this should work if we expanded the behavior
to other models. For example, Promotions
should only go out to users that are not in the paid tier
(see previous post for context). That’s in contrast to Publication
notifications, which should go out to everyone.
Since the rules for who gets notified vary depending on what they’re being notified about,
it makes sense for the notification’s subject to orchestrate it. That’s the Publication
. Responsibilities—check ✅.
Alright, so what message are we sending to the Publication
? I think it’d make sense to command
the publication to #notify
— whatever that means is up to the publication. Message—check ✅.
Putting it all together — it looks like this:
class Publication::NotificationJob < ApplicationJob
queue_as :default
def perform(publication)
publication.notify
end
end
Notice how it reads like plain English. That’s more important than it seems. We’ll explore this in future posts, but this sort of ergonomic detail adds up across the codebase. Ultimately, this helps programmers keep the whole system in their heads and communicate with other people (and LLMs) easily using plain language.
Draw The F’ing Owl
I’ll avoid pontificating further and speed through the rest of the implementation.
We’ve established Publication
should decide who gets a notification. What’s a good message
to send so that it returns its audience? Well... Why not #audience
?
And then, each member of that audience (let’s call them spectators
) should get to decide how
they want their notifications delivered (e.g. by email, push, SMS, etc). So the spectator
is our noun —
let’s send them a #notify_about(something)
message.
Looks like this:
class Publication < ApplicationRecord
belongs_to :post
delegate :blog, to: :post
def notify
audience.find_each { |spectator| spectator.notify_about self }
end
private
def audience
blog.subscribers
end
end
We made some big jumps there! And we’re still missing key details—like actually sending the email. I think going into every detail of that is too much for this post. But let me know what you're curious about, and we can dive into that in a future post.
Thinking about evolution
This is a good moment to stop and ask ourselves how this might evolve.
The point is not to solve for every possible evolution right now. But, rather, to make sure we can accommodate foreseeable evolution. If we can’t, that unequivocally means we cannot ship this code.
I can foresee us wanting to notify about other things, like promotions for our paid tier. What would change if that happened?
The job might end up looking like this:
class NotificationJob < ApplicationJob
queue_as :default
def perform(notifiable)
notifiable.notify
end
end
# Used as
NotificationJob.perform_later Publication.first
NotificationJob.perform_later Promotion.first
So, do we make the change now or later?
I’d say go with the first version until you need the second version. Just make sure you keep the possibility to easily accommodate foreseeable evolution every time you touch this code.
It’s not about generalizing
- Why are we doing this at all?
- Is this premature abstraction?
- Why bother? If we don’t even know whether we’ll be reusing this business logic
All valid questions — And they came up, very validly, after sharing my previous post.
This isn’t about generalizing—it’s about nailing down the domain model.
Have you heard the adage "watch the pennies and the dollars will take care of themselves"? That’s why we’re doing this. We’re investing in a rich domain model because that yields dividends down the line.
So far we’ve only seen one dividend in the form of readable code. You’ll have to trust me that there are many more. And we’ll explore them in future posts so be sure to follow along.