Logo Canva Austria GmbH.

Canva Austria GmbH.

Startup

Visual AI Made Simple

Description

René Koller von kaleido erläutert in seinem devjobs.at TechTalk, wie das Devteam das Konzept von MVC auf MVVC erweitert hat und wie es zum Einsatz kommt.

By playing the video, you agree to data transfer to YouTube and acknowledge the privacy policy.

Video Summary

In Visual AI Made Simple, René Koller explains how service objects using the command pattern give cross-domain business logic a proper home in Rails, avoiding fat models, bloated controllers, and logic in views. Using a plan subscription workflow with multiple payment processors, he refactors to a service with an initializer, a single public call method, private steps (validate/subscribe/notify), and a shared response object to unify error handling, enforce dependency injection, and keep it stateless and idempotent. Viewers can apply this to get simpler controllers and faster, more reliable tests by unit-testing the command and asserting on meaningful result objects instead of brittle, request-driven integration tests.

Visual AI Made Simple: Using Service Objects and the Command Pattern to tame MVC business logic

What we learned from “Visual AI Made Simple” by René Koller (Canva Austria GmbH.)

In “Visual AI Made Simple,” René Koller (Canva Austria GmbH.) offers a pragmatic answer to a recurring engineering question: where should cross-cutting business logic live when your MVC app grows up? Koller works as a lead developer in a Vienna-based visual AI unit whose mission is to make visual AI simple and easy to use for everyone. He mentions three services as current context: “RoofBG” for removing backgrounds in images, “Unscreen” doing the same for videos, and “Designify” as a next-level automation for design creation based on multiple AIs. The team uses Rails heavily. From that real-world backdrop, Koller argues for Service Objects—specifically the Command pattern—as a way to bring structure and testability to complex business flows.

From the DevJobs.at editorial vantage point, the session reads like a field guide to clean application logic: crisp goals, concrete procedure, and a test strategy that pays off immediately. The throughline is straightforward: models hold data, views present, controllers coordinate; business processes that span multiple domain objects belong in dedicated services.

The problem space: When MVC scales, cracks appear

Koller starts from the classic Model–View–Controller skeleton. It shines in small apps. But at scale, one of three anti-patterns tends to emerge:

  • Fat models: they violate single responsibility, absorbing foreign concerns and even other models or services.
  • Overgrown controllers: instead of orchestrating requests and responses, they implement sprawling workflows and error branches.
  • Logic in views: presentation logic leaks into templates, often prompting a layer of view models.

View models are already a type of service object and help with presentation logic. Yet they still don’t answer the central question: where does business logic that crosses domain boundaries go? Koller’s answer is Service Objects—focusing on the Command pattern.

Service Objects in brief: The Command pattern

There are many service object styles—adapters, commands, decorators, query objects, view models, presenters, form objects. Koller zeroes in on the Command pattern, summarized as:

A command represents and executes a business process specific to your application.

Its structure is intentionally minimal and explicit:

  • One initializer/constructor that accepts the parameters and objects the service will work on.
  • One public method—often named call—that serves as the only entry point.
  • Private methods that perform the actual work.

This tidy shape forces clarity: the command says what it does and encapsulates how it does it. Business logic becomes a cohesive, testable unit.

A concrete case: Refactoring a subscribe action

To keep things concrete, Koller walks through a fictional subscribe method—where a user subscribes to a plan on a website. The current, controller-centric flow will feel familiar:

  • Load a plan.
  • Run basic checks (is the plan available, is the user already subscribed?).
  • Perform more complex checks (can the user subscribe to this plan?), possibly already extracted.
  • Persist the subscription and update the payment processor—supporting multiple processors across regions and payment methods.
  • Catch exceptions and return errors accordingly.
  • On success, send notifications.
  • Render the final success.

Koller calls out the issues clearly:

  • Multiple error-handling styles (early returns, long ifs, scattered rescues).
  • Multiple exit points—the controller returns early in several places.
  • Deep nesting (he notes five levels) and long-range control flow jumps.

From our DevJobs.at vantage point, that’s a textbook refactoring signal: too many responsibilities in one place, insufficient structure, difficult testing.

The target design: A skinny controller, one service, one exit

Koller sets succinct refactoring goals:

  • Improve readability.
  • Improve structure.
  • Make the flow easier to understand and follow.
  • Make it easier to test.

The target controller is terse and explicit: fetch the plan, pick a payment processor, call a single service object with plan, user, and processor, and render whatever that command returns. The result: a single exit point and no nesting.

A shared protocol: The service response object

Koller introduces a uniform service response object for all service calls. Two properties matter here:

  • Services can add errors to the result.
  • A service call is considered failed if it has at least one error.

This standardizes the outward-facing protocol: controllers only need to check success or failure and display any errors. Internally, the service retains full control over how errors arise and get translated.

Inside the command: validate, subscribe, notify

The command takes plan, user, and payment processor in its initializer. It exposes a single public method—call—that orchestrates the steps:

  • validate: check prerequisites and business constraints.
  • subscribe: create the subscription and update the payment provider.
  • notify: send notifications if everything succeeded.
  • return result: return the uniform service response.

The private methods are where the work happens:

validate: check and add errors consistently

  • Early return if the result already contains an error, to avoid doing unnecessary work on bad state.
  • Check that the plan is available and that the user can subscribe.
  • In each negative case, add an error to the result—always through the same channel.

subscribe: persistence and external systems—with exceptions handled inside

  • Only proceed if the result is still considered a success.
  • Persist the subscription in the database and update the payment processor (Koller mentions different processors by region and payment method).
  • Exceptions are rescued inside the service and translated into a payment error on the result. There’s now exactly one way to make the service fail externally: add an error to the result.

notify: only if the result is still successful

  • Early return if errors exist.
  • Send notifications only when the result remains successful.

This yields a single source of truth for success and failure—the result object—simplifying controllers and clarifying tests.

Testing before: Heavy, fragile, slow

Koller contrasts before and after tests. In the old approach, you test the controller action. That implies:

  • You need a full HTTP request cycle, including authentication and login.
  • You need to visit paths, select UI elements, and click buttons—even if the business case is simply a payment error.
  • Tests are tightly coupled to markup (e.g., a button labeled Subscribe), although the real target is the business update.
  • Errors require spies/doubles and elaborate mocking.
  • Tests are slow because they traverse the entire MVC stack (Koller explicitly calls out their slowness).

In short: integration tests for everything, even when you only want to exercise a self-contained business process.

Testing after: Targeted, fast, decoupled

With service objects, the game changes:

  • You can easily force payment errors by deriving a simple payment processor variant that raises the desired exception.
  • The setup focuses solely on the objects the service truly needs: user, plans, payment processor.
  • The test calls the service directly and asserts that the result is a failure—exactly what the business case cares about.

Koller emphasizes how much easier it is to force error conditions, how much more readable the tests become, and how they speed up. From our vantage point, this is the biggest payoff: business logic becomes testable in isolation, without UI or controller noise.

Rules of thumb for robust service objects

Koller concludes the technical segment with crisp rules for good service design:

  • Exactly one public method: call (or execute/process—but just one).
  • No self-instantiation of dependencies: use dependency injection for everything.
  • Operate on objects, not IDs: avoid coupling to a specific ORM.
  • Return something meaningful: true/false can work, but a result object with errors and context is better.
  • Handle exceptions inside the service: controllers shouldn’t rescue.
  • Don’t store state in the service: keep it as functional as possible.
  • Make it idempotent when possible: repeated calls shouldn’t break state. If payment fails, you should be able to call again without leaving a half-broken state.
  • Follow the rules long enough to know when to break them for your own good.

These guidelines produce a consistent, resilient architecture that fits Rails-heavy codebases yet isn’t tied to Rails.

Why this works: Three reinforcing effects

While Koller doesn’t label them this way, his walk-through demonstrates three compounding benefits:

1) Readability. A controller that describes the process in one line—get plan, get payment processor, service.call, render result. The service itself breaks the flow into understandable steps: validate, subscribe, notify.

2) Structure. A single outward-facing result, a single public entry point, and a single translation of exceptions into errors. Fewer exit points and eliminated deep nesting make the flow predictable and easy to follow.

3) Testability. Services are testable in isolation, without logins, routing, or markup. Error cases are easy to force. Tests get clearer and faster.

Those three benefits map precisely to the refactoring goals Koller sets—and the before/after contrast makes them tangible.

Reality check: External processors without controller pain

The subscribe use case involves external systems (payment providers). Koller mentions supporting different processors (for instance by region and method) and underlines that failures can occur. The service approach shines here:

  • Through dependency injection, each payment processor arrives as an object; in tests, it’s trivial to replace or derive.
  • Exceptions raised by providers are contained and translated into a single error channel—the result. Controllers remain free of rescue logic.

This consolidates complexity where it belongs—in the command—rather than scattering it across controllers or models.

From controller action to domain operation

The mindset shift is important: a command describes a domain operation, not an HTTP interaction. Koller’s structure highlights the boundary:

  • Controller: orchestrates I/O (HTTP in, HTTP out).
  • Service/Command: performs the business operation (validate, persist, notify external systems).

That separation of concerns—transport versus domain—makes MVC manageable at scale. Many controller anti-patterns fade once you adopt this cut.

Lines that stuck with us

A few quotes and paraphrases that capture the essence:

  • “A command represents and executes a business process specific to your application.”
  • There is exactly one path to failure: add an error to the result. No side channels.
  • Early returns in each step once the result carries an error—preventing unnecessary work and follow-on failures.
  • Services should not store state and should be idempotent when possible.

Small rules, big cumulative effect. They turn “just another object” into a reliable unit.

A practical checklist: When to introduce a service object

Based on Koller’s example, a pragmatic checklist suggests itself:

  • The controller has multiple exit points or deep nesting.
  • The model is absorbing foreign logic and dependencies.
  • Error handling is scattered and inconsistent.
  • Tests require full HTTP requests, auth, and even a shadow browser—although the target is pure business logic.
  • External systems (payments, notifications) are entangled with controller/model code.

If one or more items apply, a command-style service can provide the missing clarity.

Session close

Koller closes by noting the team context—an international team with many users and substantial image/video processing volume—and mentions they are hiring, pointing to a careers page.

The technical core remains crisp: service objects—in particular the command style—give cross-domain business logic a home. Controllers slim down, models stop bloating, and tests become focused and fast.

Our takeaway

“Visual AI Made Simple” by René Koller (Canva Austria GmbH.) delivers a precise playbook for business logic in MVC apps:

  • Put cross-domain processes into service objects.
  • Use a uniform result object for error handling.
  • Keep controllers skinny and predictable.
  • Make tests focused, cheap, and robust.

The subscribe example shows how to move from a hard-to-read, deeply nested controller with many exit points to an elegant, idempotent domain operation. For teams leaning heavily on Rails and feeling the strain of growth, this approach is immediately applicable.

Follow the rules strictly at first, then adapt them consciously with experience. You’ll gain less friction, more clarity, and better tests—turning “Visual AI Made Simple” into “Business Logic Made Simple” with commands that do exactly what they say.

More Tech Talks