-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
171: Practice Exercise: Building a State Machine with AASM
When you first start using the AASM (Acts As State Machine) gem, it feels like magic. You define a few states, a few events, and suddenly your model has these handy bang methods like order.pay! or order.ship!. But that magic comes with a strict set of rules. If you try to move a record into a state that you haven't explicitly allowed, AASM won't just ignore you—it'll throw a wrench in your gears.
class Order
include AASM
aasm do
state :pending, initial: true
state :paid, :shipped, :cancelled
event :pay do
transitions from: :pending, to: :paid
end
event :ship do
transitions from: :paid, to: :shipped
end
event :cancel do
transitions from: [:pending, :paid], to: :cancelled
end
end
end
# Everything looks great... until this happens:
order = Order.new
order.ship!
# => AASM::InvalidTransition: Event 'ship' cannot transition from 'pending'
The 'Invalid Transition' Crash
I've seen this trip up plenty of developers. In the code above, the logic seems sound: you can't ship something that hasn't been paid for. But the code crashes because I tried to call ship! while the order was still pending.
AASM is designed to protect your data integrity. It assumes that if you didn't explicitly define a transition from pending to shipped, that transition is illegal. In a real production app, this crash would happen the moment a user clicks a button out of order or an API call hits your endpoint in the wrong sequence. You can't just assume the UI will prevent the user from doing this; your model needs to handle it gracefully.
Handling State Transitions Safely
You have two real options here depending on what you want the user experience to be. If the transition is truly illegal, you shouldn't let the app crash. Instead of using the "bang" method (ship!), which raises an exception, you can use the non-bang method (ship). This returns false if the transition is invalid instead of exploding.
order = Order.new
if order.ship
puts "Order is on its way!"
else
puts "Wait, this order isn't ready to be shipped yet."
end
However, if you realize that your business logic was actually wrong—say, some "VIP" orders can be shipped before payment—you need to update the transitions definition. I personally prefer using arrays for the from: option to keep things clean when multiple states can lead to the same destination.
event :ship do
# Now both pending and paid orders can move to shipped
transitions from: [:pending, :paid], to: :shipped
end
Guarding the Gate with Logic
Sometimes a state transition is legally allowed by the state machine, but only if a certain condition is met—like checking if an order actually has items in the cart. This is where guards come in. I use guards constantly because they keep the "business rules" inside the state machine rather than scattering if/else statements all over my controllers.
event :ship do
transitions from: :paid, to: :shipped, guard: :items_present?
end
def items_present?
# Only allow shipping if the order isn't empty
self.line_items.any?
end
Now, even if the order is in the paid state, order.ship! will fail if items_present? returns false. It's a much more robust way to build a workflow than just hoping the data is correct.
📋 Practical Task
Exercise: Build a Subscription Lifecycle Machine
You need to build a state machine for a Subscription model. This model tracks whether a user is in a trial period, actively paying, overdue on payment, or has cancelled their service.
Requirements:
- States:
trial(initial),active,past_due, andcancelled. - Events:
activate: Moves a subscription fromtrialtoactive.miss_payment: Moves a subscription fromactivetopast_due.retry_payment: Moves a subscription frompast_dueback toactive.cancel: Moves a subscription tocancelledfrom any other state.
- The Guard: The
activateevent should only work if a methodcredit_card_present?returnstrue. (For the sake of this exercise, just define the method to returntrue).
Your Goal: Implement the Subscription class. Then, write a small script to test that you cannot move a subscription from trial directly to past_due, and verify that the cancel event works regardless of the current state.
There are no comments for now.