Understanding Hanami's Architectural Fundamentals

Hanami's Design Philosophy

Unlike Rails, Hanami promotes a decoupled structure with multiple applications under one umbrella. Each app can have its own lifecycle, dependencies, and isolated business logic. This isolation benefits long-term maintainability but introduces hidden inter-app communication challenges.

Hanami vs. Rails Runtime Expectations

Hanami uses dry-system and dry-container for dependency injection and lifecycle management. This contrasts with Rails' global, mutable state. Misunderstanding this difference can lead to side effects, like state leakage across test boundaries or stale configuration in production servers.

Common Troubleshooting Scenarios

1. Auto-loading Failures in Production

Hanami's auto-loader (zeitwerk or dry-system) may silently fail in Dockerized or multi-threaded environments due to misconfigured inflectors or incorrect directory structures.

NameError: uninitialized constant MyApp::Repositories::UserRepository

Fix

  • Ensure correct namespace alignment: lib/my_app/repositories/user_repository.rb must match MyApp::Repositories::UserRepository.
  • Validate boot sequence with Hanami.boot explicitly in entrypoints like Rake tasks and background jobs.

2. Thread Safety Violations in Services

Hanami encourages stateless services, but improper use of shared mutable state (e.g., class variables, external caches) can cause thread bleed under Puma.

# Anti-pattern
class EmailService
  @@client = Sendgrid::Client.new

  def self.send(email)
    @@client.send_email(email)
  end
end

Fix

Move client initialization to request scope or use dry-system's container lifecycle hooks for proper isolation.

container.register("email.client", memoize: true) { Sendgrid::Client.new }

3. Bootable Components Not Loaded

Custom bootable components under system/boot are often missed due to missing registration or incorrect load order.

# system/boot/email.rb
Hanami.boot.register_provider(:email) do
  start do
    register("email.client", EmailClient.new)
  end
end

Ensure you invoke Hanami.boot before referencing the component in CLI tasks or workers.

4. Background Jobs Not Respecting Container State

When Sidekiq or custom workers bypass the application container, they often operate on stale configs or disconnected DB pools.

Fix

  • Inject Hanami.app[:container] explicitly on boot.
  • Wrap job execution in a container context.
Hanami.boot
container = Hanami.app[:container]
container.start(:persistence)
MyWorker.perform_async(args)

Diagnosing Dependency Injection Failures

Dry-System Resolver Errors

Incorrect naming or forgotten providers lead to cryptic errors like:

Dry::Container::Error: Nothing registered with key "mailer.sendgrid"

Validate registration via container.keys and enforce consistent naming via auto_register and namespace options.

Best Practices for Production Systems

  • Use bootable components for all external dependencies (mailer, cache, ORM).
  • Leverage slice-level testing to isolate bugs across multiple apps in umbrella projects.
  • Apply explicit lifecycle boundaries using dry-container lifetimes (e.g., :singleton, :request).
  • Instrument services using OpenTelemetry-compatible middlewares for visibility.

Conclusion

Hanami's elegant architecture rewards teams that embrace its modularity and container philosophy, but it also demands precision in component wiring and lifecycle management. From thread safety concerns to loader mismatches, most issues stem from mismatched mental models carried over from Rails or ad-hoc service layering. By investing in container-driven design, understanding component lifecycles, and enforcing consistent naming conventions, teams can build robust, scalable back-end services that fully harness Hanami's capabilities.

FAQs

1. Why do my bootable components fail silently?

They often fail due to being registered after container finalization or omitted during app initialization. Explicitly call Hanami.boot in non-web contexts.

2. Can I use global state safely in Hanami apps?

No. Hanami is designed around isolated dependency injection. Shared global state breaks concurrency guarantees and causes subtle bugs.

3. Why is dependency injection failing in background workers?

Workers usually skip the Hanami boot lifecycle. Manually boot the app and inject the container before invoking jobs.

4. How do I debug dry-container resolution issues?

Print container.keys to inspect registrations. Ensure provider keys match usage sites and avoid typos in nested keys.

5. Is Hanami suitable for large-scale applications?

Yes, but it requires disciplined modular design and container lifecycle hygiene. It's especially effective in service-oriented architectures.