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 matchMyApp::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.