Hanami Slices: Modular Code Organisation for Ruby Apps
In the previous tutorial, we explored persistence with ROM in Hanami. Now we turn to organising code with Hanami slices, one of Hanami 2’s core tools for structuring larger applications around clear domain boundaries.
Before you start
Slices become useful as soon as an application grows beyond a handful of models and a few controllers. At that point, the traditional app/ directory starts to hide the shape of the domain. You can still keep the project working, but the code becomes harder to reason about because unrelated features sit too close together.
Hanami slices solve that problem by giving each domain its own boundary. That boundary does not mean the slice is isolated from the rest of the app. It means the slice has a clear home for its actions, views, repositories, and helpers, which makes the code easier to navigate and easier to hand off between developers.
If you are used to Rails, the idea is similar to carving out a namespace for a large feature area, but Hanami goes a bit further by encouraging you to keep the feature boundary visible in the framework itself. That explicit structure helps when you are reviewing code or deciding where a new class should live.
What are Slices?
Slices are Hanami’s solution for code organization in medium-to-large applications. They provide modular boundaries that group related components together; actions, views, repositories, entities, and services; all working within a specific domain.
That means a slice is not just a folder convention. It is a way to keep a feature coherent from the route down to the database interaction. When a slice is designed well, you can open a directory and understand the purpose of the code without chasing files across the whole application.
Consider slices as mini-applications within your main Hanami app. Each slice:
- Has its own set of actions
- Can have its own views and templates
- Contains domain-specific logic
- Shares core infrastructure with other slices
This mental model is useful because it tells you when a new class belongs in the slice and when it should stay in shared code. If something only exists to support one business domain, the slice is usually the right home. If the code is truly generic, it can stay outside the slice and be imported where needed.
Why use slices?
As your application grows, dumping everything into the app/ directory becomes unmanageable. Slices solve this by:
- Domain boundaries ; Group code by feature or domain
- Isolation ; Each slice can be relatively independent
- Team scalability ; Different teams can work on different slices
- Clear dependencies ; Explicit about what each slice needs
Those four benefits are related. Clear boundaries make ownership easier to understand, which makes teams easier to scale. They also make refactoring less risky because you can inspect one slice without guessing which unrelated parts of the app depend on it.
When the code is still small, a single flat structure can be fine. The trouble starts when the same directory contains features that evolve at different speeds. One feature wants a new repository, another needs a new HTML view, and a third needs a job or an integration. Slices keep that growth from turning into clutter.
Creating a Slice
Generate a new slice using the Hanami CLI:
bundle exec hanami generate slice admin
This creates the following structure:
app/
├── slices/
│ └── admin/
│ ├── actions/
│ ├── views/
│ └── slice.rb
The slice file defines the slice’s configuration and is the entry point for the feature boundary. The generated structure gives you a starting point, not a final architecture. You still decide which classes belong inside the slice and which classes should stay shared. That flexibility is what makes the feature useful for apps that are growing from a simple codebase into something more domain-driven. Over time, as you add more actions, views, and repositories to the slice, the directory becomes a self-contained unit that a new developer can understand without reading the entire codebase. The configuration block inside the slice file sets defaults like the layout, asset prefix, and middleware that apply only to that slice. These settings live in the slice’s own Ruby file, which keeps the global application configuration clean and prevents cross-slice settings from colliding. The config.relations hash shown below is where you declare slice-specific options that Hanami reads at boot time.
# app/slices/admin.rb
module Admin
class Slice < Hanami::Slice
config.relations = {
layout: :admin
}
end
end
Anatomy of a Slice
A slice consists of several components:
The most important thing to notice is that every part of the slice points back to the same domain. The actions handle incoming requests, the views render output, and the repositories talk to persistence. That shared purpose is what keeps a slice from becoming just another folder full of classes.
Actions
Actions within a slice live in the slice’s namespace:
That namespacing is more than a naming style. It helps you see at a glance whether an action belongs to the admin area, the public area, or some other domain boundary. In a large app, that makes code review and debugging much faster because you do not need to infer intent from the class name alone.
# app/slices/admin/actions/dashboard.rb
module Admin
module Actions
class Dashboard < Hanami::Action
def handle(request, response)
response.render "admin/dashboard", stats: compute_stats
end
private
def compute_stats
{ users: UserRepository.new.count }
end
end
end
end
Routes for slice actions are defined in config/routes.rb:
Keeping the route definition close to the slice boundary is useful because it shows how the URL maps to the domain. You can open one file, see the route, and immediately understand which slice is responsible for the request.
# config/routes.rb
Hanami.configure do
slice :admin, at: "/admin" do
get "/dashboard", to: "dashboard"
end
end
Views and Templates
Slice views follow the same pattern as regular views:
The important difference is the context. Slice views are still plain Hanami views, but they sit inside a feature boundary, which makes it easier to keep templates and presentation helpers together. That matters when a feature grows enough to have its own layout, its own partials, or a few small formatting helpers.
# app/slices/admin/views/dashboard.rb
module Admin
module Views
class Dashboard < View
def render
html "admin/dashboard", stats: stats
end
end
end
end
Repositories and Entities
Slices can have their own repositories and entities:
This is where slices start to feel like miniature applications. A repository and entity pair can live right beside the action that uses them, which keeps the data flow obvious. You do not have to search the entire project to find the code that powers one domain-specific screen or endpoint.
# app/slices/admin/repositories/user_repository.rb
module Admin
module Repositories
class UserRepository < ROM::Repository[:users]
end
end
end
Sharing code between slices
Slices can explicitly import shared code from the main app or other slices:
Imports should stay intentional. The point of a slice is not to wall off code so tightly that reuse becomes impossible. The point is to make cross-slice dependencies obvious. When you import something, you are documenting that the slice depends on another part of the app and that the relationship is deliberate.
# app/slices/admin.rb
module Admin
class Slice < Hanami::Slice
# Import from main app
import for: :users
# Import from another slice
slice :billing, import: :invoices
end
end
You can also import specific components:
This pattern is helpful when one slice needs only a small piece of another slice. Instead of pulling in a whole namespace, you can import the class that actually matters. That keeps dependencies narrow and makes it easier to revisit them later.
# app/slices/admin.rb
module Admin
class Slice < Hanami::Slice
# Only import specific repositories
import Repositories::UserRepository, from: :users
import Entities::User, from: :users
end
end
Slice Configuration
Configure slices with their own settings:
Slice-specific configuration is where you tailor the boundary to the domain. A slice may need its own layout, its own middleware, or a dedicated service registration. Those settings are much easier to understand when they live next to the slice itself instead of being scattered across the application boot process.
# app/slices/admin.rb
module Admin
class Slice < Hanami::Slice
config.relations = {
layout: :admin,
assets: {
prefix: "admin-assets"
}
}
# Slice-specific middleware
middleware.use Admin::AuthMiddleware
# Slice-specific services
register Admin::Services::UserService
end
end
Practical example: an admin slice
Here’s how a real admin slice might look:
The admin example is useful because it mirrors a common production pattern. Many applications have a public-facing area and a privileged area with different routes, different templates, and different access rules. A slice makes that split explicit without forcing you to create a separate application.
# app/slices/admin/actions/users.rb
module Admin
module Actions
module Users
class Index < Hanami::Action
def handle(request, response)
users = user_repo.all
response.render "admin/users/index", users: users
end
private
def user_repo
Admin::Repositories::UserRepository.new
end
end
class Create < Hanami::Action
def handle(request, response)
user = user_repo.create(request.params[:user])
response.redirect_to "/admin/users/#{user.id}"
end
private
def user_repo
Admin::Repositories::UserRepository.new
end
end
end
end
end
When to use slices
Slices are ideal when:
- Building multi-tenant applications
- Creating an admin panel separate from the main app
- Implementing distinct features with their own domain logic
- Scaling teams across different features
- Building APIs for different clients (web, mobile, third-party)
Consider simpler organization if:
- Your app is small (< 10 models)
- Features are tightly coupled
- You’re prototyping
That last part matters because slices are a tool, not a requirement. If the project is still small, extra structure can slow you down more than it helps. The goal is to choose slices when they clarify the codebase, not to use them just because they sound advanced.
Summary
If you step back, slices are really about making the shape of the business visible in the code. Once the code reflects the domain more clearly, it becomes easier to split responsibilities, assign ownership, and keep the app from turning into a long list of unrelated classes.
You’ve learned how Hanami Slices help organize code in larger applications:
- Slices provide modular boundaries for grouping domain-specific code
- Each slice contains actions, views, repositories, and entities
- Explicit imports control dependencies between slices
- Slice configuration allows customization per slice
Slices are the key to building maintainable Hanami applications as they grow. Combined with actions, views, and ROM persistence, you now have all the tools needed to build complete Hanami applications.
Next steps
The next tutorial continues the Hanami sequence by showing how these pieces fit into a larger application structure. If you want to stay oriented, keep the slice directory open while you read the next guide. You will notice that the same boundary-based thinking shows up again and again: first in routing, then in actions, then in persistence, and finally in the way the app is organized as a whole.
See also
- Hanami actions and views — Request handling and presentation in Hanami
- ROM persistence in Hanami — Database layer with relations, repositories, and entities
- Hanami getting started — First steps with the Hanami framework