Fluxus [ˈfluːk.sus] is a lightweight, dependencyless library that brings use cases into your Ruby applications. The library leverages Ruby's best features alongside pure object-oriented concepts to create expressive and maintainable code.
This library takes inspiration from the Clean Architecture concepts of use cases.
Add it to your project as a dependency:
gem 'fluxus'Or install it directly:
gem install fluxusA use case represents a set of business rules that your application follows to achieve a specific goal. Fluxus provides a minimal structure to organize these rules while keeping your code clean and maintainable.
Fluxus is designed to be:
- Simple: Minimal API with a straightforward mental model
- Expressive: Clear and explicit interfaces for all operations
- Predictable: Consistent behavior with strong guarantees
- Chainable: Compose multiple use cases together elegantly
Fluxus follows two main principles:
- Explicit Success/Failure: Every use case explicitly returns a
SuccessorFailureresult - Chainable Actions: Results can be chained to build clean, sequential processing pipelines
Use Fluxus::Object for standard use cases:
class VerifyCredentials < Fluxus::Object
def call!(username:, password:)
user = User.find_by(username: username)
return Failure(type: :not_found, result: "User not found") unless user
return Failure(type: :invalid_password, result: "Invalid password") unless user.valid_password?(password)
Success(result: user)
end
end
# Using the use case
VerifyCredentials
.call!(username: "john", password: "secret123")
.on_success { |user| log_in(user) }
.on_failure(:not_found) { |error| show_error(error) }
.on_failure(:invalid_password) { |error| show_error(error) }For use cases where you want automatic error handling, use Fluxus::SafeObject:
class FetchUserData < Fluxus::SafeObject
def call!(user_id:)
user = User.find(user_id)
profile = ProfileService.fetch_profile(user)
Success(result: { user: user, profile: profile })
end
end
# Using the safe use case
FetchUserData
.call!(user_id: 123)
.on_success { |data| render_profile(data) }
.on_failure { |error| show_error("Could not load profile") }
.on_exception(ActiveRecord::RecordNotFound) { |data| redirect_to_not_found }With SafeObject, any unhandled exceptions are automatically captured and returned as a Failure result with type :exception.
Every use case returns a result object that follows the Fluxus::Results::Result contract:
Success(result: user) # Basic success
Success(type: :created, result: { id: user.id }) # Typed successFailure(result: "Invalid input") # Basic failure
Failure(type: :validation, result: errors.full_messages) # Typed failureAll result objects expose the same core methods:
result = Success(type: :created, result: user)
result.success? # => true
result.failure? # => false
result.unknown? # => false
result.type # => :created
result.data # => user objectResults can be chained to add conditional behavior:
CreateUser
.call!(params: user_params)
.on_success { |user| redirect_to(user_path(user)) }
.on_success(:created) { |user| NotificationService.user_created(user) }
.on_failure(:validation) { |errors| render :new, status: :unprocessable_entity }
.on_failure { |_| render :error, status: :internal_server_error }Type-specific hooks only run when the result matches the specified type:
ProcessPayment
.call!(amount: 100, user: current_user)
.on_success(:paid) { |receipt| send_receipt(receipt) }
.on_success(:pending) { |transaction| schedule_verification(transaction) }
.on_failure(:insufficient_funds) { |_| redirect_to_add_funds }
.on_failure(:card_declined) { |error| show_card_error(error) }When using SafeObject, you can handle specific exceptions:
ImportData
.call!(file: params[:file])
.on_success { |results| flash[:notice] = "Imported #{results[:count]} records" }
.on_exception(CSV::MalformedCSVError) { |_| flash[:error] = "Invalid CSV format" }
.on_exception { |data| Bugsnag.notify(data[:exception]) }You can chain multiple use cases together using the then method:
VerifyCredentials
.call!(username: params[:username], password: params[:password])
.then(GenerateAuthToken, expires_in: 24.hours)
.then(LogLogin, ip: request.remote_ip)
.on_success { |auth_token| cookies[:token] = auth_token }
.on_failure { |error| render json: { error: error }, status: :unauthorized }The then method passes the result data from the previous use case as arguments to the next one, merging any additional arguments you provide. This works differently based on the return type:
- If the result data is a hash, it's merged with any additional arguments
- If the result data is not a hash, it's passed as
result: datato the next use case
def process_order(params)
ValidateOrderParams
.call!(params: params)
.then(ReserveInventory)
.then(ProcessPayment)
.then(CreateShipment)
.on_success { |shipment| OrderMailer.confirmation(shipment).deliver_later }
.on_failure(:payment_declined) { |error| notify_customer(error) }
.on_failure(:inventory_unavailable) { |items| suggest_alternatives(items) }
.on_failure { |error| log_order_failure(error) }
endFluxus works great with Rails controllers:
class UsersController < ApplicationController
def create
CreateUser
.call!(params: user_params)
.on_success { |user| redirect_to user_path(user), notice: "User created!" }
.on_failure { |errors| render :new, locals: { errors: errors } }
end
private
def user_params
params.require(:user).permit(:name, :email, :password)
end
endBug reports and pull requests are welcome on GitHub at https://github.com/Rynaro/fluxus.
The gem is available as open source under the terms of the MIT License.