The letter A styled as Alchemists logo. lchemists
Published October 1, 2025 Updated September 19, 2026
Cover
Hanami Containers

This article assumes you have familiarity with Hanami and want to dive deeper into how dependencies work, are organized, and managed via containers. At a high level, containers allow you to define your dependencies once so you can quickly reference and use them throughout your application.

There are two primary categories to be aware of when thinking about containers in Hanami: Components and Providers. For example, here’s a quick and dirty way to see the differences:

# demo/app/aspects/demo.rb

module Demo
  module Aspects
    class Demo
    end
  end
end

# demo/config/providers/demo.rb

Hanami.app.register_provider :demo do
  start { register :demo, Object.new }
end

With the above, we have a Demo application that has a core Demo component and a demo provider. Neither of these do anything since they are generic objects…​and, yes, this is heavy use of demo terminology but bear with me because here’s how you’d access and use them when jumping into the Hanami console:

# Component (demo/app/aspects/demo.rb)
Hanami.app["aspects.demo"]  # #<Demo::Aspects::Demo:0x00000001391987e8>

# Provider (demo/config/providers/demo.rb)
Hanami.app.start :demo
Hanami.app[:demo]           # #<Object:0x000000013ad52ba0>

The rest of this article will unravel what the above is doing and why this is powerful.

Components

If you’ve spent any amount of time on this site, you know much has been written about containers, dependencies, and automatic injection over the years. The goal isn’t to repeat what’s already been written but to highlight and explain how this works in Hanami. For those that might need to get up speed, please read about Dependency Injection Containers first. Feel free to dive deeper by reading the documentation of these two gems as well:

  • Containable: Explains how containers work along with full implementation details.

  • Infusible: Explains how automatic injection builds upon what’s already registered within your containers.

When you combine the power of the above gems — and returning to the earlier code snippet — this means you can do the following:

# demo/app/aspects/demo.rb

module Demo
  module Aspects
    class Demo
    end
  end
end

# app/actions/dashboard/show.rb

module Demo
  module Actions
    module Dashboard
      class Show < Demo::Action
        include Deps[:demo]
      end
    end
  end
end

Use of Deps which, confusingly, is short for Dependencies, not Departments, is the primary container for all of your dependencies within your Hanami application. This means, when you use include Deps[:demo] at the top of your class, you get automatic injection of the demo dependency. In other words, what the Containable and Infusible gems do for you in any Ruby application.

Defaults

By default, Hanami provides the following dependencies — made available via the Deps container — for immediate use:

  • assets: Allows you to access assets such as images, icons, and so forth.

  • inflector: Allows you to customize object inflection so you can use API, for example, for your module and class names instead of Api.

  • logger: Allows you to log messages within your various objects.

  • notifications: Allows you to instrumentation.

  • routes: Provides named route URL helpers based on what is defined in your routes (i.e. routes.rb),

  • settings: Allows you to provide custom settings unique to your application in a type safe manner.

Having default dependencies gets you off the ground and running quickly. Where this becomes problematic is when you want to swap them out with better implementations. Unfortunately, this is something you can’t do without a lot of effort. That said, one example of how you can achieve this is documented in Hanami Logging.

Registration

By default, Hanami will automatically register any object in the app directory for use as a dependency for future injection. This can also be disabled entirely or customized as befits your needs.

Automatic

The way automatic registration works — and is explained further in the Providers section — is by using each object’s path as a key for identification. For example, let’s say we have the following structure:

app
  /repositories
    /device
    /firmware
    /screen

The above would be registered as the following keys:

"repositories.device",
"repositories.firmware",
"repositories.screen"

Notice none of the keys use an "app" prefix. This is because automatic registration only works for files in the app directory and would be redundant to include so saves you some typing.

Now that you know what the keys are (which is essentially a path using dot notation), this means you can include any or all of these dependencies within another object. If we return to our earlier example, this means you could inject the "firmware" repository as follows:

# app/actions/dashboard/show.rb

module Demo
  module Actions
    module Dashboard
      class Show < Demo::Action
        include Deps["repositories.firmware"]
      end
    end
  end
end

The above would give you immediate access to the firmware repository but you could inject the other repositories too by separating each with a comma.

Should you ever need to see the full list of all your dependencies, you can always jump into the Hanami console and run the following:

# Necessary to force all dependencies to be loaded (lazy loads by default).
Hanami.boot

# Lists all dependency keys (including providers).
Hanai.app.keys

Again, if you need more on this, read about Dependency Injection Containers if you haven’t already.

Disablement

There are times when you need to prevent Hanami from automatically registering an object. This can be for several reasons such as:

  • Any object you never want to inject into another object.

  • An abstract superclass that will never need to be instantiated directly and only exists to provide common functionality to all subclasses.

  • A Proc which will always yield this error: 'Dry::System::Loader.call': undefined method 'new' for an instance of Proc (NoMethodError).

  • Any object that can’t respond to .new like Dry Schema, proc, lambda, method, etc.

When you find yourself in any of the above situations, you can disable automatic registration by adding the following to the top of any file:

# auto_register: false

💡 See the Pragmater gem documentation if you’d like to learn more about pragmas.

If you find that you need to exclude an entire directory, you can update your app’s configuration as follows:

# config/app.rb

module Demo
  class App < Hanami::App
    config.no_auto_register_paths.append "schemas"
  end
end

The above ensures all objects within the app/schemas directory (and subsequent sub-directories) are completely ignored.

Lastly, if none of the above is appealing, you can place your objects within the lib directory of your application since nothing that goes in that directory will be registered. This directory is still managed by Zeitwerk so you don’t have to add a require to the top of your files. The lib directory is also where you can create your own set of custom dependencies using Containable + Infusible which gives you a lot more control over behavior.

Memoization

By default, all application components are memoized. You can change this behavior by adding the following pragma to the top of your objects’s source file:

# memoize: false

The above will ensure your object is not memoized.

💡 See the Pragmater gem documentation if you’d like to learn more about pragmas.

Additionally, you can disable memoization by namespace. For example, you could add the following to your application configuration as follows:

# config/app.rb

class App < Hanami::App
  config.no_memoize = %w[jobs schemas]
end

The above ensures any dependency that starts with "jobs" or "schemas" won’t be memoized. You add fine grained control by using a block:

class App < Hanami::App
  config.no_memoize = -> component { component.key.start_with? "jobs" }
end

💡 Use of the memoize pragmet on an individual component always takes precedence over the no_memoize configuration.

Providers

Providers allow you to register additional components beyond the automatic registration of your application’s components with the following behavior:

  • Ability to register a specific instance of an object as a component for dependency injection.

  • Ability to configure a component that might require multiple steps or other dependencies in order to be setup correctly.

  • Ability to tap into the prepare, start, and stop hooks of the provider lifecycle.

Here’s a simple example where the Redis gem is defined for use as a dependency:

# config/providers/redis.rb

Hanami.app.register_provider :redis do
  prepare { require "redis" }

  start { register :redis, Redis.new(url: slice[:settings].redis_url) }

  stop { slice[:redis].shutdown }
end

The above taps into all provider lifecycle steps. You’ll also notice that this provider is defined as redis.rb within the config/providers directory which allows Hanami to discover and use the provider. Each step of the lifecycle will be explained shortly.

Naming

The name of your provider is important because if there is a mismatch between the file name and the key used to register your provider, you’ll get an error. Consider the following:

# demo/config/providers/a_demo.rb

Hanami.app.register_provider :demo do
end

Notice the file name is a_demo.rb but demo is used when registering the provider. If you were to start this provider using the registered key as follows:

Hanami.app.start :demo

You’d end up with the following error:

Provider :demo not found (Dry::System::ProviderNotFoundError)

This can be confusing to debug so ensure your provider file name and key always match.

Loading

Provider load order isn’t guaranteed to be deterministic. For the most part, providers load in alphabetical order based on file name. For example, the following will be executed in the order listed:

a.rb
b.rb
c.rb

You don’t want to rely on this information because it might change but also because you can always force a specific provider dependency to be prepared or started within your own provider’s lifecycle which alleviates having to know when a provider is loaded.

Lifecycles

A lifecycle consists of the following steps:

  • Prepare

  • Start

  • Stop

You can trigger any of these steps by jumping into the Hanami console and running the following:

app = Hanami.app

app.prepare   # Triggers all providers prepare blocks.
app.boot      # Triggers all providers start blocks.
app.shutdown  # Triggers all providers stop blocks.

Unfortunately, provider method names are not always consistent with the corresponding lifecycle steps and would definitely be nice to see this issue fixed in the future. Each lifecycle is explained in greater detail below which will highlight these inconsistencies further.

ℹ️ When in the development environment (i.e. console) or test environment (i.e. RSpec), the providers will not be available. This is by design because Hanami provides only the minimum by running the prepare lifecycle by default to remain fast. You can use Hanami.boot to fully load the application including all providers.

Prepare

This lifecycle step is meant for preparing providers for use which usually means using require to load dependencies. Example:

# config/providers/redis.rb

Hanami.app.register_provider :redis do
  prepare { require "redis" }
end

If you jump into the Hanami console, you can then prepare the above redis provider multiple ways by using one of the following:

# Explicit require but more cumbersome to type.
require "hanami/prepare"

# Direct messaging of the app.
Hanami.app.prepare

# Syntactic sugar to prepare the entire app. Doesn't accept arguments.
Hanami.prepare

# Prepare a specific provider.
Hanami.app.prepare :redis

You can also check if your application is prepared or not:

Hanami.app.prepared?

Start

This lifecycle step is meant for configuring and registering your providers. Example:

# config/providers/redis.rb

Hanami.app.register_provider :redis do
  start { register :redis, Redis.new(url: slice[:settings].redis_url) }
end

If you jump into the Hanami console, you can then start the above provider multiple ways by using one of the following:

# Explicit require but more cumbersome to type.
require "hanami/boot"

# Direct messaging of the app. Doesn't accept arguments.
Hanami.app.boot

# Syntactic sugar to prepare the entire app. Doesn't accept arguments.
Hanami.boot

# Start a specific provider. Yes, inconsistent method name.
Hanami.app.start :demo

⚠️ Starting/booting the app will also freeze the container(s) which means you can’t add anything further to the container after the start lifecycle has completed.

You can also check if your application is started or not:

Hanami.app.booted?

Stop

This lifecycle step is meant for stoping and cleaning up your providers. Example:

# config/providers/redis.rb

Hanami.app.register_provider :redis do
  stop { slice[:redis].shutdown }
end

If you jump into the Hanami console, you can then stop the above provider multiple ways by using one of the following:

# Direct messaging of the app. Doesn't accept arguments.
Hanami.app.shutdown

# Syntactic sugar to stop the entire app. Doesn't accept arguments.
Hanami.shutdown

# Stops a single provider. Yes, inconsistent method name.
Hanami.app.stop :redis

Unlike the prepare or start steps, you can’t inquire if the application has been stopped but you’ll definitely get plenty of errors if you stopped the app and tried to continue running regardless.

Undefined

We’ve discussed all lifestyle steps but there is one more which is undefined. Example:

Hanami.app.register_provider :demo do
  puts "I have no lifecycle step!"
end

The above is worth knowing about but is not recommended because whatever code you put here will be run outside of any step. In fact, the above will be run only once which can happen at any step which leads to confusing and indeterministic behavior. Again, don’t use.

Namespaces

To use namespaces, add namespace: true as a keyword argument. Example:

Hanami.app.register_provider :persistence, namespace: true do
  start do
    register "config", Hash.new
    register "db", :database_demo
  end
end

With the above, we’ve registered the persistence namespace which has both config and db defined within it. This is identical to how Containable namespaces work which allows you to organize multiple objects within the same namespace. To confirm, you can always jump into the Hanami console and see for yourself:

Hanami.app.start :persistence
Hanami.app.keys

# [
#   "persistence.config",
#   "persistence.db"
# ]

Had we not enabled a namespace, we’d end up with config and db as top-level keys. Example:

# Register without namespace

Hanami.app.register_provider :persistence do
  start do
    register "config", Hash.new
    register "db", :database_demo
  end
end

# Verify

Hanami.app.start :persistence
Hanami.app.keys

# [
#   "config",
#   "db"
# ]

As you can see, this is confusing, lacks readability, and doesn’t show these components are related.

There are situations where you might not want to use namespace: true and manually create the namespace yourself. This is especially useful when you need to register an object at root of your namespace. Example:

# config/providers/i18n.rb

Hanami.app.register_provider :i18n do
  start do
    register :i18n, I18n
    register "i18n.t", I18n.method(:translate)
  end
end

Notice the namespace key isn’t used but dependencies are registered via the i18n and i18n.t keys. Use of i18n means we can register an object at the namespace root (since we also register i18n.t within the namespace shortly after). See the i18n Example, below, for full details.

⚠️ One downside of not using a namespace means you have to use Hanami.start :i18n in IRB, at the top of your specs, an so forth in order to gain access to the i18n.t object because using Hanami.app["i18n.t"] won’t work as you’d expect without the starting the provider first. This isn’t a problem when using namespaces.

Dependencies

Once your providers are registered, associated dependencies can be accessed directly via the app like any other dependency. Example:

Hanami.app[:redis]

You can also inject provider dependencies like you would any other dependency. Example:

include Deps[:redis]

Locals

All providers have access to local variables that you can interact with during each lifecycle which can be inspected as follows:

# demo/config/providers/debug.rb

Hanami.app.register_provider :debug do
  prepare do
    config              # #<Dry::Configurable::Config values={}>
    slice               # Demo::App
    target              # Demo::App
    target_container    # Demo::App
    container           # #<Dry::Core::Container:0x00000001389b49c8 @_container={}>
    provider_container  # #<Dry::Core::Container:0x00000001389b49c8 @_container={}>
    callbacks           # {before: {prepare: []}, after: {}}
  end
end

Each of the above can be explained as follows:

  • config: This is a copy of the slice’s configuration which is copied at time of first access.

  • slice: Provides access to the current slice’s container. For the main application, this means slice is a convenience wrapper around Hanami.app which save you some typing. When in a slice other than the main application, then the slice is relative to the slice that your provider is located in. You’ll notice that target and target_container do the same thing. Stick with slice so you can use the same terminology regardless of the slice you are currently in. The other nice thing is you can use slice to access a dependency either in the application or another provider. Example: slice[:logger]. This is most useful in the start lifecycle step because if the logger hasn’t been started, it will be and then made available for immediate use.

  • container: Provides access to the provider container which only houses provider specific dependencies. The provider_container yields the same result at the cost of being more to type.

  • callbacks: These are misleading because they don’t work and appear to be vestiges of Dry System. If callbacks did work, you’d be able to define before and after callbacks for the prepare and start lifecycle steps.

Memoization

As with components, providers are memoized by default when you don’t provide a block to the register method. On the other hand, when using a block, you’ll need to specify memoization. For example, the following syntax is equivalent and will ensure memoization:

# Without block.
Hanami.app.register_provider :demo do
  start do
    register :demo, Object.new
  end
end

# With block but with memoize set to true.
Hanami.app.register_provider :demo do
  start do
    register :demo, memoize: true do
      Object.new
    end
  end
end

If you were to jump into the Hanami console — use either syntax from above — you’ll see the object ID remains the same:

Hanami.app[:demo].object_id  # 19968
Hanami.app[:demo].object_id  # 19968
Hanami.app[:demo].object_id  # 19968

The only way to register a dependency without memoization is to use a block with memoize set to false. Example:

Hanami.app.register_provider :demo do
  start do
    register :demo, memoize: false do
      Object.new
    end
  end
end

If you were to jump into the Hanami console — using the above provider — you’d see that you get a new instance each time:

Hanami.app[:demo].object_id  # 9080
Hanami.app[:demo].object_id  # 9784
Hanami.app[:demo].object_id  # 10488

That fact that using or not using a block causes different behavior is bug. This includes the fact that you can’t disable memoization when not using a block. For example, this does not disable memoization: register :demo, Object.new, memoize: false.

Knowing how memoization works is important but be careful when using or not using blocks.

Conditionals

Providers can be conditionally registered using the if keyword argument as shown below:

Hanami.app.register_provider :demo, if: Hanami.env?(:production) do
end

With the above, the demo provider would only be registered if in a production environment. The if keyword argument only takes a value that evaluates to true and does not accept Proc objects.

If you are wondering if you could use a unless keyword argument, much like you’d do with native Ruby conditional logic, the answer is no. Only the if keyword is allowed for conditional logic.

Inheritance

In situations where your provider has more involved lifecycles and you want test coverage, you can create your own implementation. At a high level, this is what you need to know:

  • Use app/providers as the namespace for quick lookup and organization of your implementations.

  • Subclass Hanami::Provider::Source in order to implement your custom solution.

  • Each subclass expects a provider_container, target_container, and slice to be injected as keyword arguments.

  • You still need to register your provider via config/providers/demo.rb, for example, and is simple as adding a one-liner. Example: Hanami.app.register_provider :demo, source: Demo::Providers::Demo.

To illustrate, here’s an example of Cogger as a logger provider (and documented in greater detail via Hanami Logging):

# app/providers/logger.rb

# auto_register: false

module Demo
  module Providers
    class Logger < Hanami::Provider::Source
      RESOLVER = proc { Object.const_get "Cogger" }

      def initialize(environment: Hanami.env, resolver: RESOLVER, **)
        @environment = environment
        @resolver = resolver
        @id = Hanami.app.namespace.to_s.downcase.to_sym
        super(**)
      end

      def prepare = require "cogger"

      def start
        add_filters
        register :logger, build_instance
      end

      private

      attr_reader :environment, :resolver, :id

      def add_filters = logger.add_filters :csrf, :password, :password_confirmation

      def build_instance
        io = "log/#{environment}.log"

        case environment
          when :test
            logger.new(id:, io: StringIO.new, formatter: :json, level: :debug).add_stream io:
          when :development then logger.new(id:).add_stream(io:, formatter: :json)
          else logger.new id:, formatter: :json
        end
      end

      def logger
        @logger ||= resolver.call
      end
    end
  end
end
RSpec Example

The following is the specification to the above implementation:

# spec/app/providers/logger_spec.rb

require "hanami_helper"

RSpec.describe Demo::Providers::Logger do
  subject(:provider) { described_class.new provider_container:, target_container:, slice: }

  let(:provider_container) { Dry::Core::Container.new }
  let(:target_container) { Dry::Core::Container.new }
  let(:slice) { Hanami.app }

  describe "#prepare" do
    it "answers false due to already being loaded" do
      expect(provider.prepare).to be(false)
    end
  end

  describe "#start" do
    let(:cogger) { class_spy Cogger }
    let(:hub) { instance_spy Cogger::Hub }

    before { allow(cogger).to receive(:new).and_return hub }

    it "adds filters" do
      provider = described_class.new(
        environment: :test,
        resolver: proc { cogger },
        provider_container:,
        target_container:,
        slice:
      )

      provider.start
      expect(cogger).to have_received(:add_filters).with(any_args)
    end

    context "with test environment" do
      subject :provider do
        described_class.new environment: :test,
                            resolver: proc { cogger },
                            provider_container:,
                            target_container:,
                            slice:
      end

      it "initializes" do
        provider.start

        expect(cogger).to have_received(:new).with(
          id: :demo,
          io: kind_of(StringIO),
          formatter: :json,
          level: :debug
        )
      end

      it "adds stream" do
        provider.start
        expect(hub).to have_received(:add_stream).with(io: "log/test.log")
      end
    end

    context "with development environment" do
      subject :provider do
        described_class.new environment: :development,
                            resolver: proc { cogger },
                            provider_container:,
                            target_container:,
                            slice:
      end

      it "initializes" do
        provider.start
        expect(cogger).to have_received(:new).with(id: :demo)
      end

      it "adds stream" do
        provider.start
        expect(hub).to have_received(:add_stream).with(io: "log/development.log", formatter: :json)
      end
    end

    context "with any other environment" do
      subject :provider do
        described_class.new environment: :production,
                            resolver: proc { cogger },
                            provider_container:,
                            target_container:,
                            slice:
      end

      it "initializes" do
        provider.start
        expect(cogger).to have_received(:new).with(id: :demo, formatter: :json)
      end
    end

    it "registers logger" do
      provider.start
      expect(provider_container.key?(:logger)).to be(true)
    end
  end
end

To register the above implementation, use the following:

# config/providers/logger.rb

Hanami.app.register_provider :logger, source: Demo::Providers::Logger

The key takeaways with this implementation are:

  • The # auto_register: false pragma is important in order to avoid unnecessary components being registered that will never be used directly.

  • The RESOLVER constant is injected to lazy resolve the Cogger constant at runtime as the logger instance variable. This is important for a couple reasons:

    • Ensures your prepare lifecycle step has time to require the Cogger dependency while also ensuring provider registration doesn’t fail with a NameError due to Cogger not being required during initialization. Otherwise, you’d be manually requiring the Cogger gem which defeats the purpose of the prepare step.

    • Allows you to use RSpec Test Doubles for testing purposes. Otherwise, testing your provider would be much more difficult.

  • The environment is injected which also aids testing so you can quickly write specs for each supported environment.

  • You only need to implement the lifecycle methods for your public interface: prepare, start, and stop. In this example, stop isn’t needed.

  • The remaining logic is specific to Cogger setup which ensures proper encapsulation.

A few additional notes on the above:

  • You only need to inject empty Dry::Core::Container instances for the provider and target containers.

  • You can use Hanami.app for the slice which is the main application slice. If you were doing this in a specific slice, you’d use the slice, itself, instead of Hanami.app.

  • The prepare spec doesn’t do a lot since Cogger will be loaded by the time your test suite runs but nice to the kick the tires.

  • The start spec is where you are able to test the implementation by using spies. Again, this is made possible by being able to inject the resolver and the environment. Something you definitely can’t do without a ton of effort when not using a subclass.

You won’t always need a specialized provider class so you should default to using simple provider block registration syntax within the config/providers folder of your application. Otherwise, if your provider lifecycles are much more involved, definitely subclass to the app/providers folder so you can apply test coverage while still being able to register your custom provider.

Slices

Slices are out of scope for this article but is worth mentioning that they too can have their own providers by using the same directory structure within each slice. Example:

slices/demo/config/providers/demo.rb

Inspection

You can inspect your providers and associated objects by jumping into the Hanami console and use the following:

# List all providers.
Hanami.app.container.providers

# Acquire specific provider.
Hanami.app.container.providers[:htmx]

# Acquire specific provider's container.
Hanami.app.container.providers[:htmx].container

# List application specific files.
Hanami.app.container.providers.provider_files

You can also use Hanami.prepare, Hanami.boot, or Hanami.shutdown to inspect at different lifecycles.

Examples

The following is a collection of examples that might be of interest to learn about the various ways you can leverage different providers.

Alba

The following is an example of using Alba for serialization:

# config/providers/alba.rb

Hanami.app.register_provider :alba do
  prepare { require "alba" }

  start do
    Alba.backend = :oj
    Alba.enable_inference! with: :dry
  end
end

Cogger

The following uses the Cogger gem to provide a custom — and enhanced — logger to replace Hanami’s default logger:

Hanami.app.register_provider :logger do
  prepare { require "cogger" }

  start do
    Cogger.add_filters :csrf, :password, :password_confirmation
    register :logger, Cogger.new(id: :demo)
  end
end

The above is a simple example but, if you need more functionality, check out the Inheritance section above or Hanami Logging for a deeper dive.

HTMX

The following uses the HTMX gem for processing HTTP headers, building DOM elements, and more.

Hanami.app.register_provider :htmx do
  prepare { require "htmx" }

  start do
    toggler = lambda do |request, default = "app"|
      HTMX.request?(request.env, :request, "true") ? false : default
    end

    register :htmx, HTMX
    register :htmx_defaults, {"defaultSwap" => "outerHTML"}.freeze
    register :htmx_layout, toggler
  end
end

HTTP

The following uses the HTTP gem for processing HTTP requests and responses.

Hanami.app.register_provider :http do
  prepare do
    require "connection_pool"
    require "http"
  end

  start do
    slice.start :logger

    http = ConnectionPool::Wrapper.new size: ENV.fetch("HANAMI_MAX_THREADS", 5) do
      HTTP.timeout(connect: 2, read: 10, write: 10)
          .use(:auto_inflate)
          .use(logging: {logger: slice[:logger]})
          .headers("User-Agent" => "http.rb/#{HTTP::VERSION} (#{Hanami.app.app_name})")
    end

    register :http, http
  end

  stop { slice[:http].close }
end

Mail

The following uses the Mail gem to provide email functionality.

Hanami.app.register_provider :mail do
  prepare { require "mail" }

  start do
    Mail.defaults do
      case Hanami.env
        when :development then delivery_method :smtp, address: "127.0.0.1", port: 1025
        else delivery_method :test
      end
    end

    register :mail, Mail
  end
end

If using Docker, you might want to add Mailpit as a service:

mail:
  image: axllent/mailpit
  ports:
    - "1025:1025" # SMTP Server
    - "8025:8025" # Web UI
  restart: unless-stopped

Redis

The following is an example of adding Redis as a provider where you can leverage environment variables and settings to configure the provider.

Start by adding the following to your environment:

REDIS_PASSWORD=secret
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_DB=0
REDIS_URL="redis://:${REDIS_PASSWORD}@${REDIS_HOST}:${REDIS_PORT}/${REDIS_DB}"

Next, add a setting:

module Demo
  class Settings < Hanami::Settings
    setting :redis_url, constructor: Types::Params::String
  end
end

Last, setup your provider by levering both the environment and setting:

# config/providers/redis.rb

Hanami.app.register_provider :redis do
  prepare { require "redis" }

  start { register :redis, Redis.new(url: slice[:settings].redis_url) }

  stop { slice[:redis].shutdown }
end

Rollbar

Maybe you are using Rollbar for exception monitoring and would like to have it configured for use in production. Here’s an example of what that might look like:

# config/providers/rollbar.rb

Hanami.app.register_provider :rollbar, if: Hanami.env?(:production) do
  prepare { require "rollbar" }

  start do
    Rollbar.configure do |config|
      config.access_token = slice[:settings].rollbar_token
    end

    slice[:logger].add_backend Rollbar, log_if: :exception?
  end
end

The above accomplishes the following:

  • Ensures Rollbar only starts in production.

  • Adds Rollbar as an additional logger.

Sequel

Hanami will automatically configure the database provider for you but you can customize the default provider by using the same key (i.e. :db) to alter and enhance further. In this case, we teach Sequel to default to the UTC time zone.

# config/providers/db.rb

Hanami.app.configure_provider(:db) { Sequel.default_timezone = :utc }

You could also ensure an in-memory SQLite database is automatically migrated when the database starts:

# config/providers/db.rb

Hanami.app.configure_provider :db do
  after :start do
    slice["db.gateways.default"].auto_migrate! slice["db.config"], inline: true
  end
end

Delving further, you could register additional extensions for your database:

# config/providers/db.rb

Hanami.app.configure_provider :db do
  config.gateway :default do |gateway|
    gateway.adapter :sql do |adapter|
      adapter.extension :pg_enum
    end
  end
end

Specs

Due to providers not being fully loaded when running your test suite, you might need to load on demand. One way to handle this is by adding the following configuration:

# spec/hanami_helper.rb

RSpec.configure do |config|
  config.before :each, :with do |example|
    containers = Hanami.app.slices.each.map(&:container).prepend Hanami.app.container

    Array(example.metadata[:with]).each do |provider|
      containers.each do |system|
        system.start(provider) if system.providers.find_and_load_provider provider
      end
    end
  end
end

The above would allow you to dynamically use any provider on demand. Example:

# Specification
it "is an example that needs Redis", with: :redis do
  # Specification details go here.
end

Guidelines

When using providers, adhere to the following:

  • Avoid lifecycle blocks that are multiple lines long. These are meant to be lightweight and should not be overly complicated to implement. Otherwise, the provider you are trying to register is too complicated and needs to be simplified or extracted to it’s own implementation as discussed when using Inheritance.

  • Avoid using providers only for configuration logic as that should go in app.rb or in an initializer. The other reason is providers execute in a non-deterministic order which can lead to order of operation issues.

Conclusion

Dependency Injection Containers are extremely powerful and now you know how to use them in Hanami. You’ve also learned how they are automatically registered, how you can disable them, and how you can use more sophisticated forms of dependency injection via providers. All of this combined together gives you a nice way to break out your dependencies into smaller, maintainable, and testable objects that can be reused throughout your application.