As part of my journey towards modularity, I am scrutinizing the boundaries between domains. I am now starting to comprehend the problems that can be introduced in larger systems by simply handing out ActiveRecord collections to any consumer that wants one. To use the simplest possible example, lets say you have a BlogPost record. Over time, your application grows from just a simple web publishing interface to now have a JSON:API, a GraphQL service, syndication to third party blogging platforms, an RSS endpoint, an Admin interface, and a mobile app that uses a different JSON API than your public one. Thus, all of these consumers depend on the interface of BlogPost. In short, some of the issues you now have are:
- Because ActiveRecord serves up columns as attributes, all of these layers or consumers depend directly on your database structure.
- You have exposed columns that may be depended on that were not intended to be part of the public API, even if you aren’t directly exposing them (perhaps they are used in queries or decorators.)
- You may have named scopes or queries, but they may have ‘hidden’ dependencies that one of your call-sites forgot about, e.g. we correctly filtered out unpublished posts but not ones from archived accounts.
- Although your call-sites may not be doing this, you have given them an object that can be used to delete or update your record.
- You have given out an object that can access any relation, and any relation of that relation, and thus essentially your consumers may be secretly depending on the implementation of practically your entire application or data model
- (You get the idea, but more perhaps on validations and inconsistent update / insert patterns)
So, while I still think that ActiveRecord is extremely convenient and ergonomic, I am wanting to avoid passing out or taking in ActiveRecord objects at the boundaries of packs.
To solve this issue, my inclination is to use something like a Repository Pattern. In our example perhaps, BlogPost::Repository. It would likely have a few responsibilities:
- Fetch one by ID
- Fetch many by IDs
- List (a paginated interface)
- Ordering by known public orderings
- Filtering by known public filterings
- Return ‘dumb’ objects, e.g. Data or dry-struct
So, it is intended to solve a few issues.
- First, it prevents exposing ActiveRecord objects, and rather returns simpler attribute objects that conform to a stable ‘public’ interface, even if there are changes to the underlying ActiveRecord model(s) or database tables.
- Second, it limits sorting and filtering to known, supported, testable concepts. For example, perhaps here the ‘published’ filter takes into account the archived account consideration, and perhaps doesn’t even expose the concept of archived accounts at all. It may still use Query Objects and Scopes behind the scenes.
- Third, it still provides a somewhat ergonomic (though slightly less so) interface to a collection of ‘the stuff I want for this API endpoint’. Hopefully, it leads to consistent implementation of hooking up ‘Repository Backed Resources’ to GraphQL resolvers, JSON:API resources, controllers, etc.
With all that, my question(s) are – is this something teams have an established pattern for, or a gem that is working well for them? Is Data sufficient, or is dry-struct or something else preferable? How do you handle ‘related entities’ without re-inventing ActiveRecord and includes? How are you designing filtering, sorting, and paging from an API perspective? (It seems to me that ‘chaining’ behavior is maybe too dangerous, rather than a single method call with required attributes.)
Thanks for reading!