Hanami persistence proposal for 2.x

I know this is an old thread, but I would like to respond to @svoop’s recent post because he raises interesting issues. Also, I feel like my views have matured (somewhat) and I will be better able to articulate them clearly (and succinctly–I hope :roll_eyes: no promises).

I want to pause first to thank the core team for the amazing progress over the past year. I appreciate all of your hard work! I am so excited to see where Hanami will go–and where it will allow me and others to go.

Over the past year I used Hanami 2 to build a tiny production app (a few hundred dollars so far :partying_face:, but I think I still need to find a real job . . .). I followed the patterns laid out in @timriley’s DecafSucks and @bkuhlmann’s Hemo app, and just tried to learn as much as I could. Having done that, I have to say that my original views here and in other threads on ideas for organizing Hanami apps have only been reinforced.

I found Hanami by literally trying various Google searches for Ruby frameworks and “clean architecture” (because those were the only words I knew to describe what I wanted to learn about). This brought me to Hanami 1 during the period that Hanami 2 was still in the planning phase. This should tell you something about my mindset and the path I was looking to explore.

Hanami 1 vs Hanami 2

In Hanami 1, we had a separate home for “business logic”: it lived under /lib/project-name/. In Hanami 2, there is a different approach. There is a special, default “slice” that lives under /app, and one or more other slices (optionally) that live under /slices.

@timriley and the others have put in a ton of work to make it so that anything you can do in /app, you can also do in any of your slices. The real distinction is in the autoloading: everything in /app is available everywhere, code in /app can call anything (including code in slices), but code in slices can only call to other slices where explicitly permitted. (Please correct me if that is not accurate.)

It seems that the recommended convention is to break up your app by placing complete “vertical slices,” or some subset of functionality, into separate slices. This is cool, but I wanted to explore something a little different.

I would like to pursue a pattern that is more consistent with the Hanami 1 approach. I call it “Shell & Core” (or "Core & Shell, depending on how I’m feeling :thinking:) based on Gary Bernhardt’s well-known talk, “Functional Core, Imperative Shell.” I’m not sure that I’m following Gary’s advice exactly, but the name is succinct and describes, in general, what I would like to do. Which is . . .

Shell & Core

I would like to keep all my UI/App kind of stuff in the /app directory. All actions, views, mailers, channels, what-have-you. I think of this as the “shell.” I would like to keep all of my “business logic” (the “core”) in one or more slices. Touching on @svoop’s question, I would name each slice based on its broad area of concern for the domain, like “identity,” “sales,” “claims,” etc. But in the beginning, I just start with a “main” slice.

ASIDE: I love the exploration of modularity concepts that slices allows (and Packwerk allows in the Rails space), but I think that future experience will show that including UI and other “shell” concerns in slices will only lead to coupling and hard-to-maintain code bases.

As for the “core” (i.e., slices), I would like to explore a pattern that I first saw laid out in a wonderful (and underappreciated) talk by Peter Bhat Harkins at RailsConf 2015, “What comes after MVC?.” (Peter is the maintainer of lobste.rs and is/was a consultant.) He argued for splitting business logic into four basic object types: shell (more on that in a sec), values, entities, and adapters.

The values, entities, and adapters all readily map to existing objects in Hanami: types, entities/structs (when persistence is implemented), and providers. I would use the more general term “adapters,” and place the Hanami providers and ROM relations and repositories in this directory, although I haven’t worked out the sub-directory structure for these yet.

By “shell” objects, Peter is referring to command-like objects that orchestrate the other object-types to carry out actual work. In Hanami 1, this role was filled by Interactors. In Hanami 2, I would guess that this role will be filled by the dry-operation project (which appears to be the spiritual successor to dry-transaction and the dry-monads “Do Notation”).

Since dry-operation is not yet ready, I hacked together a “proto-interactor” to fulfill this roll. The name of the object is totally arbitrary, but what I really wanted was the “Params” behavior of Hanami Actions, the Do Notation of dry-monads (to do my work inside of the ProtoInteractor), and the ability to return a dry-monads result object.

That turned out to be pretty easy to put together. I am now (just starting, actually) using this object and Peter Harkins’ patterns to refactor my tiny app (before growing it further). The resulting folder structure could look something like this:

slices/
├── identity/
│   ├── adapters/
│   ├── entities/
│   ├── types/
│   ├── identify_member.rb
│   └── ...
├── sales/
│   ├── adapters/
│   ├── entities/
│   ├── types/
│   ├── [interactor].rb
│   └── ...
└── claims/
    ├── adapters/
    ├── entities/
    ├── types/
    ├── [interactor].rb
    └── ...

In this way, the “use cases” (ProtoInteractors in my case) of each slice would be listed right in the root directory of the slice. If a slice grows out of hand, the interactors and other objects could be further sub-grouped in namespaces within the slice–think multiple DDD-style aggregates in a slice/domain.

How I (am trying to) use it

Like this:

  • Actions (or whatever other endpoint) in /app call one or more interactors (two to four is probably typical) to retrieve data or make changes, and use that information to execute views.
  • Each slice (which could potentially have it’s own database–I haven’t actually tried that yet) would be constrained to it’s assigned area of responsibility.
  • I can use HTMX to separate out portions of a page to call their own REST endpoints so actions don’t need to be so busy collecting all the data for every page–my version of “islands.”
  • I want my actions to be almost as boring as routing tables (similar to the pattern @solnic tooted about recently).

All of this is possible through existing Hanami/Zeitwerk autoloading.

Wrap up

I don’t know if @svoop’s suggestion of multiple, custom-named slice directories is feasible (it probably is), but I don’t think I would need it for the pattern described here. I might like to rename /slices to /core, but that’s hardly necessary.

I am very excited about my current explorations. I think this pattern could be very useful and could provide an answer for dealing with increasing complexity in large/growing apps that is currently lacking in the Ruby ecosystem. I haven’t sorted out all of the details yet, and I’m leaving some things out here for brevity (if you can call it that :roll_eyes:). I plan on writing one or more blog articles with more details on this approach but, you know, time . . . :unamused:

I am certainly not advocating for this pattern for every Hanami app, or any new app at all for that matter. I love the flexibility that Hanami 2 currently offers. For small or new apps, keep everything in /app! Relations, repositories, the whole bit! Handle your params and business logic right in the actions (or other objects) and rock out! However, when you start to grow, I think the patterns laid out above by smarter people before me are really compelling–and Hanami can also take you there!

A final note on managing UI complexity, I think namespaces under /app are more than up to the task (and that modularizing UI is probably overkill). You could easily have something like:

app/
├── members/
│   ├── actions
│   ├── views
│   └── mailers
├── member_admins/
│   ├── actions
│   ├── views
│   └── mailers
└── admin/
    ├── actions
    └── views

I think this would currently break Hanami generators and routing helpers, but I believe it is already compatible with autoloading.

Those are my thoughts. Thank you for reading this far. :relieved: If any of this sounds interesting to you, I would love to discuss these ideas with someone (okay, anyone :laughing:).