Choosing a media-creation architecture for a Rails monolith
Shopify's admin is a Ruby on Rails monolith that hundreds of developers work on, with a continuous deployment cycle used by millions of people. When the platform recently launched native support for videos and 3D models on product pages — alongside the 7 billion images already stored — the engineering team had to integrate new media types without disturbing the legacy image infrastructure.
A central design question emerged: should media creation be driven by ActiveRecord callbacks or by a pipeline of dedicated service classes? Both approaches were evaluated against a backdrop of safe database operations, where transactions are used to make writes reliable.
The case for callbacks
Rails callbacks trigger logic at specific points in an object's lifecycle — when it is created, saved, updated, deleted, validated, or loaded. For a single media type, the pattern is simple: one image object has a polymorphic association to a media object, and creation requires a transaction so both records exist together. Adding a video type introduces unique properties such as thumbnails, which forces conditionals into the models to distinguish between media types.
This is where the callback approach breaks down. Even in a small example, the media object ends up with granular knowledge of the details of videos versus images. As more media types are added, business logic spreads across multiple models and becomes intertwined with lifecycle hooks. The order in which callbacks fire during each state becomes hard to track, making the code difficult to maintain and debug at scale.
The pipeline alternative
The team instead adopted a pipeline architecture, where the output of one class becomes the input to the next. Each class handles exactly one operation for one media type. The structure mirrors a restaurant kitchen: a head chef (entry-point service) receives orders, a sous chef (media create handler) routes each order to the appropriate specialist, and each specialist handles only its own domain — the pastry chef knows nothing of roasting.
All pipeline classes share the same three-method skeleton:
before_transaction— preparation work before any record is persistedduring_transaction— the actual database writesafter_transaction— cleanup following a successful operation
The video create handler, for example, follows this pattern and is concerned only with creating videos. Moving up, the media create handler calls the appropriate per-type handlers, creating a cascading effect. The highest-level product create media service exposes a single public entry point and knows nothing about individual media types.
Because the entire pipeline runs inside a transaction, interdependent objects for a given product are created together, much like dishes for a whole table arriving at once.
What the pipeline buys
This structure yields several concrete benefits:
- Independent components can create media and manage their own transactions.
- System components interact with media through a service entry point rather than accessing media models directly.
- Media callbacks no longer fire alongside those of the caller's models, so the flow of execution is explicit rather than hidden by framework magic.
- Each class is a plain Ruby object with a narrow job, lowering cognitive load for readers and making debugging straightforward.
- The order of create, update, and delete operations is controlled by the pipeline rather than by implicit callback chains.
The trade-off is clear. Callbacks deliver fast, easy setup that suits simple use cases, and Rails' abstraction can accelerate development. But in a large production application with evolving requirements, callback-driven logic becomes a tangle of conditionals spread across ActiveRecord models. The pipeline, by contrast, confines each media type's logic to its own class and establishes a separation of concerns that keeps the media component's internals opaque to the rest of the application.
When designing such a pipeline, the entry point should be singular and obvious to consumers. The pipeline should perform one expected action without side effects — creating media and nothing else. Delegating subtasks to small, focused steps reinforces the separation of concerns within the pipeline itself.



