What extraction means in this app
A rule, and the code shape it produces
This project's MEMORY.md states its architecture as a Layer Contract. Business logic lives in services, which return Result = Data.define(...). Read-only, cross-model questions live in query objects, which return the same kind of Result. Models keep validations, scopes and associations, with no business logic. Controllers handle HTTP and call one service for a write or one or more queries for a read. Read as a rule, that's abstract. Read against the app's files, it's a specific, checkable code shape, and this course spends its first five lessons on what that shape looks like before asking an AI tool to produce it.
The pattern, counted
Grep this app's app/services and app/queries directories and a convention shows up on its own. 97 of the 124 Ruby files in those two directories define Result = Data.define(...), often as their first line, and expose a self.call class method that builds and returns one. The rest are other kinds of objects: policies, small value objects and a few narrow helpers that don't need a success-or-failure result. For most of the code, "extraction" in this app has a literal shape:
module SomeNamespace
class SomeService
Result = Data.define(:success, :data, :errors)
def self.call(...)
# the actual logic
Result.new(success: true, data: ..., errors: [])
end
end
end
A controller action that calls SomeNamespace::SomeService.call(...), checks result.success and renders or redirects does its whole job in a few lines. The logic that decided success or failure never touches the controller.
Why a query object is a separate thing from a service
Both share the Result/self.call skeleton, but they answer different questions. A service changes something, such as awarding XP, saving a partner or posting a comment, and reports whether the write succeeded. A query object answers a read, often across more than one domain, without writing anything. This app's Enrollment::LeaderboardQuery is the clearest example. It reads Identity::User and Enrollment::ExperiencePoint in the same method to build the leaderboard rows, a cross-domain question that belongs neither in Identity:: nor in a model method. Lesson 5 starts from that file.
What "extraction" means, concretely
Not every controller needs it. A controller that already delegates cleanly has nothing left to extract, and recognizing that is as much a skill as finding something to pull out. Extraction is what happens when logic that decides something sits outside a service or query object. That might be a conditional business rule, a multi-step calculation or a cross-domain read, sitting inline in a controller action, on a model beyond its data shape, or duplicated in two places that should share one. Lesson 2 looks for that in three of this app's controllers.
What this course covers
- Auditing three of this app's controllers for extraction candidates, and reporting what's found as it is.
Partners::FetchSiteIconService, a single-responsibility service, read closely.Enrollment::SubmitQuizService, a service that orchestrates other objects instead of reimplementing their work.Enrollment::LeaderboardQueryandEnrollment::UserGamificationQuery, query objects in theEnrollmentnamespace, and why the first one's namespace doesn't match a model it reads.- Prompting an AI tool for a new extraction that matches this app's conventions instead of a generic shape.
- Putting the workflow together, from audit to a reviewed, working extraction.
Keep going with lesson 2
Auditing this app's controllers for extraction candidates
Audits three controllers line by line and finds that each one already delegates its decisions to services.
A free account also gives you the quizzes, your progress and notes, and the discussion.