Install the enterprise integer department
Ruby already has +=. Add the gem for the ordered hold, durable alarm, and
restart recovery surrounding our ambitious little operator.
bundle add solid_objects
bin/rails generate solid_objects:install
bin/rails db:migrate
bin/rails solid_objects:doctor
Escalate arithmetic into a domain model
One actor owns one event's availability. The hold and its keyed reminder commit with
the counter change, which is a great deal of ceremony for + 1 and exactly
enough for an expiring reservation.
We could increment a column. We could also forget which customer owns the hold, release it twice, and rediscover scheduled jobs during an incident. Choices abound.
class TicketCounter < SolidObjects::Actor
attribute :available, default: 1
attribute :holds, default: {}
def hold(buyer:)
return { held: false, available: } if available.zero? || holds.key?(buyer)
self.available -= 1
self.holds = holds.merge(buyer => Time.current.to_i)
schedule(at: 10.minutes.from_now, key: buyer).expire(buyer:)
{ held: true, available: }
end
def expire(buyer:)
return available unless holds.key?(buyer)
self.holds = holds.except(buyer)
self.available += 1
end
end
Restart Rails before the punchline
The direct hold is caller-assisted. The ten-minute reminder needs the Solid Objects runtime. Start it, place the hold in a console, then stop the runtime before expiry for maximum theatrical tension.
bundle exec solid_objects start
counter = TicketCounter.ref("show-42")
counter.hold(buyer: "ada")
counter.available
Stop Terminal 1. Leave it down past the deadline, restart the same command, then ask
counter.available again. The due reminder runs and availability returns
to 1. The expiry checks that the hold still exists, so retries are
harmless instead of arithmetically creative.
The counter was never the hard part.
The useful part is one event identity owning the guard, the hold, and the delayed
release. If the work fits inside with_lock, use it. Ten minutes is a poor
transaction duration and an excellent reminder deadline.
Seriously, for a moment: This same shape appears in inventory holds, carts, account workflows, rate limits, expiring sessions, job leases, games, and connected devices. Keeping state, delayed work, retries, and per-identity ordering correct through deploys and worker crashes is genuinely non-trivial. That is the problem Solid Objects is meant to solve.
Read the complete Rails guide →