The boundary that already exists
The repository pattern is a way to keep data access out of everything else. Before judging it, measure how much of that this application already has. The numbers below were taken on 2026-09-28.
The measurement
query objects: 58
of which define a Data Result: 51
ActiveRecord calls in views: 0
ActiveRecord calls in controllers: 22
service objects: 55
There is no data access in the view layer, across 100 templates. That is the result the repository pattern is usually sold on, and this app got it without a repository.
Twenty-two calls sit in controllers, across 57 controller files. Lesson 5 sorts them.
What a query object looks like here
module Catalog
class ListCoursesQuery
Result = Data.define(:courses_by_level, :lesson_counts, :total_counts_by_level)
def self.call(authenticated: true)
courses = ::Catalog::Course.active.ordered.includes(:category)
...
end
end
end
A class method, named arguments, and a Data object as the return value. Fifty-one of the fifty-eight follow that shape, and none returns a relation.
The part that is not what it looks like
courses_by_level holds Catalog::Course records. The Data object is a container for ActiveRecord models. It does not replace them.
That is deliberate. MEMORY.md says so directly:
"Queries must return pure Data objects or Arrays of ActiveRecord Models. They are purely Domain operations and must never contain presentation logic, HTML formatting, or URL routing."
The rule forbids presentation logic and allows ActiveRecord. That is a clear position, and it differs from the repository pattern's. The two often get mixed up, so it's worth saying plainly.
flowchart TB
V["views: 0 data access calls"] --> B["the boundary that matters"]
C["controllers: 22 calls in 57 files"] --> B
Q["58 query objects, 51 with a Data Result"] --> B
Q --> N["the Data object contains ActiveRecord models"]
N --> P["a shape boundary, not a persistence boundary"]
Two different boundaries
This distinction runs through the whole course.
A shape boundary says what a caller receives: three named fields rather than whatever a controller happened to assign. The caller can't reach for anything it wasn't given, and the query's contract reads in one line.
A persistence boundary means the caller can't tell where the data came from. It returns domain objects with no save, no where and no associations to traverse.
This application has the first everywhere and the second nowhere. A controller holding courses_by_level is holding Course records, and it could call .update! on one.
Which one buys what
| Shape boundary | Persistence boundary | |
|---|---|---|
| Data access out of views | Yes | Yes |
| The query's contract is readable | Yes | Yes |
| A caller can traverse to anything | Yes, it can | No |
| Swap the ORM | No | In principle |
| Test the domain without a database | No | Yes |
| Cost | A Data.define line |
A mapping layer per aggregate |
Most teams want the first two rows, and a query object gives them for free.
Rows three, four and five are the repository pattern's territory. Lesson 2 asks what each is worth in a Rails application specifically, because the answers differ from those in the language the pattern was written for.
Where this leaves the codebase
This codebase has solved the problem the repository pattern solves with a cheaper tool, and it left one property on the table.
That property: a Course handed to a controller is still a Course. It is mutable, traversable and connected to the database. Whether that matters is the open question, and lessons 3 to 6 answer it four different ways.
The measurement itself is settled. No data access in 100 templates is a real boundary, and a convention got it there, not an abstraction.
Key takeaways
- Measured on 2026-09-28: 58 query objects, 51 with a
DataResult, 0 data access calls in views, 22 in controllers. - The
DataResult contains ActiveRecord models. It is a shape boundary, not a persistence boundary. MEMORY.mdforbids presentation logic in queries and explicitly allows returning models.- A shape boundary gives readable contracts and keeps data access out of views. A persistence boundary hides the ORM.
- This codebase has the first everywhere and the second nowhere, by convention rather than abstraction.
Keep going with lesson 2
What the pattern promises
Reviews the pattern's four promises and why stacking Data Mapper on Active Record causes friction in Rails.
A free account also gives you the quizzes, your progress and notes, and the discussion.