Open-source project
u-gems/u-case avatar
u-gems/u-case

u-case turns a use case into a Ruby class with attributes, a call method and a typed result

Represent use cases in a simple and powerful way while writing modular, expressive and sequentially logical code.

552 stars31 forksRubyMIT

At a glance

What is it?
u-case is a Ruby gem that models a use case as a Micro::Case subclass with declared attributes, a call! method and a Success or Failure result, with composition through flows and then-chains, transactions on demand and optional exception capture. It suits a Rails developer moving business logic out of controllers, and the project has committed publicly to never making a breaking API change.
Who is it for?
u-case fits a Rails developer who wants business logic to live in named, testable classes with typed results rather than in controller methods, and who wants to compose those classes without callbacks or mutable state. It does not fit a project that wants a framework level opinion on application structure, because u-case deliberately stays at the level of a single class and leaves routing, dependency injection and the rest of the architecture alone.
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 143 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

Attributes, call!, and Success or Failure

The README states the shape of the whole library in one sentence: attributes, a call! method, Success(...) or Failure(...). Everything else in the README is described as a way to make that shape easier to compose, validate, observe and transact.

That is a good way to evaluate a library like this. If the core shape does not match how you think, the composition features will not rescue it. If it does match, the extras are exactly the things that normally get hand-rolled badly.

A use case here is a class inheriting from Micro::Case, with declared attributes and a call! method that returns a result.

ruby
require 'u-case'

class Slugify < Micro::Case
  attribute :title, accept: String

  def call!
    slug = title.downcase.strip.gsub(/[^a-z0-9]+/, '-').gsub(/^-|-$/, '')

    slug.empty? ? Failure(:blank_title) : Success(result: { slug: })
  end
end

Slugify.call(title: 'Hello, World!')
# => #<Micro::Case::Result success? type=:ok data={ slug: "hello-world" }>

The `accept:` option on an attribute is the validation hook. Passing something that is not a String does not raise; it produces a failure result, which the next snippet shows by branching on the type of failure.

ruby
Slugify
  .call(title: 42)
  .on_success { puts it[:slug] }
  .on_failure(:invalid_attributes) { warn it[:errors] }
# warn: { "title" => "expected to be a kind of String" }

The failure carries a type, :invalid_attributes here, and a payload with the errors keyed by attribute name. That is a structured contract rather than an exception, and it is the reason a caller can distinguish a bad input from a business rule rejection without parsing messages.

Pattern matching on results is the primary branching style

Ruby pattern matching works on the results out of the box, and the README presents it as the recommended way to branch rather than the callback helpers.

ruby
case Slugify.call(title: 'Hello, World!')
in { success: _, result: { slug: } }
  redirect_to "/posts/#{slug}"
in { failure: :invalid_attributes, result: { errors: } }
  render status: 422, json: { errors: }
in { failure: :blank_title }
  render status: 422, json: { error: 'title required' }
end

The result destructures into `success:` or `failure:` keys with the payload under `result:`, and the failure type is matched as a literal symbol. Because the shapes are fixed, the pattern matching is exhaustive in practice: if you add a new failure type to a use case, the match statement that handles it becomes visible as an unmatched case rather than a silently unhandled error path.

This is the part of the design that separates u-case from service object patterns in other languages. A typical service object raises an exception or returns nil for the failure case, and the caller has to know which is which. Here both outcomes are values of the same type, and the caller matches on them.

The library also offers `on_success` and `on_failure` chains as a convenience, but the README frames them as an alternative rather than the main path, and the pattern matching form is the one to reach for when you have several outcomes to handle.

Structured attributes, and inheriting the feature mix

Attributes are not limited to scalars. You can declare a nested structure with a block, and the child attributes inherit the host's feature mix.

ruby
class CreateOrder < Micro::Case
  UUID = -> { it.is?(String) && it.match?(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/) }

  attribute :uuid, accept: UUID

  attribute :customer do
    attribute :name,  accept: String
    attribute :email, accept: String
  end

  def call!
    transaction do
      customer = Customer.create_or_find_by!(email: customer.email) { it.name = customer.name }

      order = Order.create!(uuid:, customer_id: customer.id)

      Success result: { customer:, order: }
    end
  end
end

Two things stand out. The `accept:` option takes any callable, so the UUID check is a lambda using the `it` block parameter, not just a class. And the nested `attribute :customer do` block builds a sub-structure with its own name and email attributes, which is how a use case declares a composite input without inventing a separate form object.

The `it` block parameter deserves a note, because it is the most recent Ruby feature the examples lean on and it is not available on older runtimes. The README has a section explaining that the gem itself supports Ruby >= 2.7, and that if you are on an older runtime the examples read back to the classic form. The same applies to the pattern matching syntax in the previous section.

Ruby >= 2.7 and Rails >= 6.0 are the supported ranges, with the badges also claiming the head versions.

Composition through flows, then-chains and transactions

The README says use cases compose three ways, and the two named mechanisms are flows and `Result#then` chains.

A flow is an ordered list of use case classes, and a single kwarg puts the whole flow inside a transaction.

ruby
SignUp = Micro::Cases.flow(transaction: true, steps: [
  NormalizeParams,
  CreateUser,
  CreateProfile
])

The transaction can also be scoped to a single call! with an inline block, in which case the steps inside it run atomically.

ruby
class CreateUserWithProfile < Micro::Case
  attribute :name, accept: String
  attribute :email, accept: String
  attribute :password, accept: String
  attribute :password_confirmation, accept: String

  def call!
    transaction {
      create_user
      .then(CreateProfile)
    }
  end

  def create_user
    user = User.create(name:, email:, password:, password_confirmation:)

    user.persisted? ? Success(result: { user: }) : Failure(result: { user: })
  end
end

The `then` call passes the previous result into the next use case, which is the chaining mechanism that does not require a flow declaration.

The inline transaction example is worth reading for the failure handling. The user creation returns Success or Failure based on `persisted?`, and a failure short-circuits the transaction, so CreateProfile never runs against a user that was not created. That is the whole argument for putting the work in a use case rather than in a controller method: the failure path is a value that composes, and the rollback follows from the transaction boundary.

Every flow also records each step's input and output, accessible through `transitions`, which is how you get visibility into a composed pipeline without instrumenting each case by hand.

Result contracts, exception safety and declared return types

Beyond the core shape, the README lists a set of capabilities that each answer a specific complaint about service objects.

Result contracts let you declare which types and values a use case can return, so the set of possible outcomes is written down rather than inferred. Combined with pattern matching, that turns the failure modes of a use case into something a reader can check.

Exception safety is opt-in. `Micro::Case::Safe` turns unhandled exceptions into `:exception` failures, which means a use case can opt into having every outcome be a Result. The default is not to do this, which is the right default: most exceptions in a use case are bugs rather than business outcomes, and swallowing them by default would hide them.

There is also the `attribute` failure type, :invalid_attributes, which is produced by the attribute layer rather than by your code, and it carries a hash of messages keyed by attribute name.

What you do not get is a lifecycle. The README is explicit that the library is immutable and callback-free, with no lifecycle callbacks, data flowing forward and nothing mutating in place. That is a constraint rather than a limitation, and it is the reason the composition mechanisms are explicit: a flow is a list of steps, a then-chain is a sequence, and there is no hidden before or after hook that could surprise you.

The claim of speed is tied to the same design. The README points at the benchmarks section and notes there is no global state, which is a reasonable basis for a library that is meant to be called from many places in a request.

The no breaking changes commitment, and where the redesign goes

The most unusual thing about the project is a promise in the README's IMPORTANT callout: no breaking API changes, ever.

The statement says that from here on, u-case's public API and runtime contracts won't break, and that the gem's role is to remain a stable, backward-compatible foundation for the projects that already depend on it. Any rethink of the abstractions belongs in solid-process, described as a redesign that applies what has been learned since u-case was created, and not in a future u-case 6.x.

The corollary is spelled out: major version bumps signal only that a Ruby or Rails version was dropped from the supported matrix, which per SemVer is a dependency-floor change, and your code keeps working. The full statement is on issue #131.

That is a genuinely rare commitment from a gem at version 5.x, and it changes how you evaluate the dependency. The usual risk with a small library is that the author redesigns it and your upgrade becomes a rewrite. Here the exit is explicit: if you want the new abstractions, you move to a different gem rather than wait for a breaking 6.x.

The repository supports the claim in its shape. There is a benchmarks/ directory, an Appraisals file for testing against multiple dependency versions, a gemfiles/ directory, a Rakefile, a test/ directory and a u-case.gemspec. The documentation table lists an unreleased version, 5.7.1 and 4.5.2, each with its own README on a version branch, so older major lines keep their documentation.

There is also a Portuguese translation of the README, a code of conduct, a CHANGELOG, and a CLAUDE.md at the root. The repository has no GitHub releases, and the last push was on 2026-06-02.

Where u-case is the wrong choice

Three situations argue against it.

The first is a codebase that has already solved this problem. u-case is a small library with a specific shape, and if your project already has a service object layer with a convention your team agrees on, the gain is a shared vocabulary rather than new capability. The README points at a real Rails application, from-fat-controllers-to-use-cases, as the case for the library, and that framing is honest about who it is for.

The second is a need for a framework opinion. u-case gives you one class shape and some composition. It does not tell you where to put things, how to inject dependencies, how to version your API, or how to organise a large domain. The comparisons/ directory in the repository suggests the author has thought about how this sits against other approaches, but the library itself stays deliberately small. If your problem is that your application structure is undefined, this is not the tool that defines it.

The third is a heavy reliance on exceptions. The opt-in `Micro::Case::Safe` exists, but the default behaviour is that an unhandled exception propagates. If your codebase's conventions are exception-based and your team will not adopt result matching, the friction will be permanent.

The alternative is plain Ruby. A use case can be a plain class with an initializer and a method that returns a value or raises, tested with ordinary unit tests, and the surrounding application decides how to translate failures into HTTP responses. The difference in approach is that u-case buys you a typed result, structured attribute validation, transactions and composition, at the cost of learning a small API and accepting that every outcome is a value rather than an exception. For a Rails team whose controllers have grown fat and whose business logic is hard to test in isolation, that trade is usually worth it. For a service with one workflow, it is more machinery than the job needs.

Editorial conclusion

u-case fits a Rails developer who wants business logic to live in named, testable classes with typed results rather than in controller methods, and who wants to compose those classes without callbacks or mutable state. It does not fit a project that wants a framework level opinion on application structure, because u-case deliberately stays at the level of a single class and leaves routing, dependency injection and the rest of the architecture alone. Start with the Slugify example from the README, and read the statement on issue #131 before you upgrade, because the project's rule is that a major version bump only ever drops a Ruby or Rails version from the supported matrix.

Frequently asked questions

How do I install and use the u-case gem?

The gem is published on RubyGems at rubygems.org/gems/u-case, and the README's quick start begins with `require 'u-case'`. It supports Ruby >= 2.7 and Rails >= 6.0, and the getting started guide is in the project wiki.

What does a u-case use case look like?

A class inheriting from Micro::Case with declared attributes and a call! method that returns Success(...) or Failure(...). The result is a typed value carrying a success flag, a type symbol and data, which you can branch on with Ruby pattern matching.

How do I compose u-case use cases?

Three ways, according to the README: declare a flow with Micro::Cases.flow and a list of steps, chain with Result#then, or wrap work in a transaction. `Micro::Cases.flow(transaction: true, steps: [...])` puts every step of a flow in one ActiveRecord transaction.

Will a u-case major version break my code?

The project states there will be no breaking API changes, ever, and that a major version bump only signals a Ruby or Rails version being dropped from the supported matrix. Redesigns go into the separate solid-process gem rather than into a u-case 6.x.

Does u-case catch exceptions for me?

Not by default. Exception safety is opt-in through `Micro::Case::Safe`, which turns unhandled exceptions into :exception failures, so a use case can opt into having every outcome be a Result.

Official sources

  1. Issues
  2. License: MIT
  3. Project website
  4. README
  5. u-gems/u-case on GitHub
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/u-gems-u-case.svg)](https://hysenlabs.com/projects/u-gems-u-case)

Was this useful?