light-service: turning a Rails controller into a list of actions
Series of Actions with an emphasis on simplicity.
At a glance
- What is it?
- LightService is a Ruby gem that decomposes a service object into a series of single-purpose actions run in order by an organizer, with expects and promises for the keys that pass between them. It suits a Rails team whose controllers have grown past a single responsibility and want the flow visible at the top of the file.
- Who is it for?
- light-service is a good fit for a Rails codebase whose controllers do several things in sequence, because the decomposition it produces makes the order of operations visible and makes each step testable on its own.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 32 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The argument starts with a controller that does too much
The readme opens with a question and a wall of Rails code, and the code is the argument. The example controller finds an order, looks up tax ranges for the region, renders an edit action with an error if the ranges are missing, calculates the tax percentage, renders a different error if that is missing, applies the tax to the order, gives free shipping if the total with tax passes a threshold, and redirects. The documentation says this controller violates the single responsibility principle all over, and asks what it would take to test the thing. It then notes the obvious fix and why it is not the fix: you could move the finders and the calculation into the tax model, but then the model logic becomes heavy. The observation about order is the one that matters for the design, since you cannot calculate the order tax without the percentage, and a framework that cannot express that ordering constraint is a framework whose steps you will eventually reorder by accident.
The organizer is the list, and the list is the documentation
The reformulated version of that controller is a bare list of three names:
(
LooksUpTaxPercentage,
CalculatesOrderTax,
ProvidesFreeShipping
)The documentation says this block should tell you the story of what is going on in the workflow, and the whole framework exists to make it possible to write code that way. The first object you need is an organizer that sets up the actions in order and executes them one by one, and the second is the actions themselves, each with one method that does one thing. The organizer in the example extends the organizer module, takes the input in a call method, and passes the context to a reduce over the three action classes:
class CalculatesTax
extend LightService::Organizer
def self.call(order)
with(:order => order).reduce(
LooksUpTaxPercentageAction,
CalculatesOrderTaxAction,
ProvidesFreeShippingAction
)
end
endThe with call is what sets up the context, and the reduce call is what runs the actions in the order they are listed. That ordering is the whole point, and it is why the actions are classes rather than lambdas, since a class name in the list tells a reader what the step is.
Expects and promises are the contract between steps
Each action declares what it reads and what it writes, and the documentation has a section for each with the expected keys and the promised keys. An action that needs the order declares expects :order, and an action that produces the tax percentage declares promises :tax_percentage, which is what tells the next action it can rely on the value. The example action shows both in one class, and the failure handling inside it is the pattern worth copying:
class LooksUpTaxPercentageAction
extend LightService::Action
expects :order
promises :tax_percentage
executed do |context|
tax_ranges = TaxRange.for_region(context.order.region)
context.tax_percentage = 0
next context if object_is_nil?(tax_ranges, context, 'The tax ranges were not found')
end
endThe value is set to a default first, so the promise is satisfied even when the lookup fails, and the failure is raised through the context with a message, and the next context returns from the block so the action stops there. The documentation has a section on default values for optional expected keys, which is the escape hatch when a step genuinely depends on something that may be absent. Once the actions exist, the controller becomes a matter of calling the organizer and checking the result, and the example shows the shape, with a result object that has a failure predicate and a message, so the controller renders the error or redirects.
Stopping the series, and the difference between skipping and failing
The table of contents separates three ways to end a run, and the distinction between them is the part of the design you have to get right. Failing the context is the mechanism the example uses, where a message is recorded and the remaining actions do not run. Skipping the rest of the actions is a different thing, for the case where the work is already done and the remaining steps are redundant rather than impossible. Skipping all the rest of the actions is for the case where the whole remaining series is unnecessary. The distinction matters because they have different outcomes for the caller. A failure means the workflow did not complete, and the caller is expected to handle an error, while a skip means the workflow completed early and successfully, and the caller is not. Confusing the two produces controllers that show an error message when nothing went wrong. The framework gives you the primitives, and the readme documents them separately, which suggests the author expects people to reach for the wrong one at first.
Hooks, benchmarks and the places the framework reaches beyond the basics
There are several features that make the gem usable in a real application rather than only in a tutorial. Before and after action hooks let you run code around each action, which is where logging, instrumentation and timing go. Around advice is the mechanism for benchmarking actions, so you can measure the cost of a step in a series rather than only of the whole organizer. Logging is a separate section, and error codes are supported, which is the difference between a workflow that fails and one that fails with a code your application can branch on. Localizing messages is supported through an adapter, with a built-in adapter, an I18n adapter for Rails and a custom adapter for anything else, and the point of it is that a failure message set in an action can be translated on the way out rather than at the point of failure. Rollback is supported too, which is the feature that makes a series of actions safe when an early step has a side effect that a later step depends on. ContextFactory is documented as a way to speed up action testing, and Rails support includes generator support for organizers and for actions, including an advanced action generator.
Requirements, installation and the Rails version caveat
The gem requires Ruby 2.x, and the generators require Rails 5 or later, with the documentation being precise that they are tested on Rails 5.x and 6.x only and will probably work on versions as old as 3.2. That is a dated statement, and it tells you the generator path is the part of the gem most likely to need attention on a current Rails. Installation is a line in the Gemfile and a bundle install:
gem 'light-service'bundle installThe gem is on rubygems as light-service with a download count badge in the readme header, and the repository holds the gem specification, a Rakefile, an rspec configuration, a rubocop configuration and a spec directory. There is a separate release notes file at the root, and the history behind the project is stated in the readme, where the author says he gave a talk at RailsConf 2013 on simple and elegant Rails code and told the story of how LightService was extracted from the projects he had worked on. The project description is a series of actions with an emphasis on simplicity, and the last push was on 2026-09-21, with no GitHub releases published, so the rubygems version is what you track.
Editorial conclusion
light-service is a good fit for a Rails codebase whose controllers do several things in sequence, because the decomposition it produces makes the order of operations visible and makes each step testable on its own. It is the wrong choice for a team that wants a different shape, since the expects and promises convention is a design system rather than a library, and the wrong choice for a codebase where the steps are not genuinely sequential, because a pipeline of actions with a shared context is more structure than an ordinary service object. It is also the wrong choice if you are on a modern Rails version and want the built-in alternatives, since the gem's own documentation states the generators are tested on Rails 5 and 6 only. Before adopting it, read the expects and promises rules, because the convention is what makes the actions composable and getting it wrong produces actions that cannot be reordered. The gem is MIT licensed, the last push was on 2026-09-21, and the repository publishes no GitHub releases, so the rubygems version is the marker to track.
Frequently asked questions
What is LightService and what problem does it solve?
It is a Ruby service skeleton framework that decomposes work into a series of single-purpose actions run in order by an organizer, so a controller that looks up a tax percentage, calculates tax and then grants free shipping becomes a readable list of three names. The documentation argues the controller violates the single responsibility principle and is hard to test, and that moving the logic into the model just makes the model heavy.
How do expects and promises work in LightService?
An action declares expects for the context keys it reads and promises for the keys it writes, and the documented example declares expects :order and promises :tax_percentage. The promised value is set to a default before the lookup so the promise holds even on failure, and there is a separate mechanism for default values on optional expected keys.
What is the difference between failing the context and skipping actions in LightService?
Failing the context records a message and the remaining actions do not run, and the caller is expected to handle an error, while skipping the rest of the actions means the workflow completed early and successfully. Skipping all the rest applies when the whole remaining series is unnecessary, and the readme documents these as three separate ways to end a run.
Does LightService work outside Rails?
The gem requires Ruby 2.x and has no Rails dependency for its core, and the Rails-specific parts are the generators, which require Rails 5 or later and are tested on Rails 5.x and 6.x only. Features such as the I18n localization adapter are for Rails, while a custom adapter covers other setups.
How do I install LightService?
Add gem 'light-service' to your Gemfile and run bundle install. The gem is published on rubygems as light-service, the project is MIT licensed, and the repository has no GitHub releases, so the rubygems version is the release to track.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/adomokos-light-service)Was this useful?