<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="pt_BR"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://alinemarques.dev/en/feed.xml" rel="self" type="application/atom+xml" /><link href="https://alinemarques.dev/en/" rel="alternate" type="text/html" hreflang="pt_BR" /><updated>2026-08-23T17:47:27-03:00</updated><id>https://alinemarques.dev/feed.xml</id><title type="html">Aline Marques</title><subtitle>Desenvolvedora Full-Stack | Apaixonada por tecnologia, inovação e aprendizado contínuo.</subtitle><author><name>Aline Marques</name></author><entry xml:lang="en"><title type="html">Automated Testing: types, TDD, and testability</title><link href="https://alinemarques.dev/en/automated-testing" rel="alternate" type="text/html" title="Automated Testing: types, TDD, and testability" /><published>2026-08-23T00:00:00-03:00</published><updated>2026-08-23T00:00:00-03:00</updated><id>https://alinemarques.dev/testes-automatizados.en</id><content type="html" xml:base="https://alinemarques.dev/automated-testing"><![CDATA[<p>One of the most common reasons people give for writing automated tests is to prevent regressions, basically, making sure a new change doesn’t break a behavior that already worked before.</p>

<p>But just knowing that tests prevent regressions doesn’t help much when it’s time to decide what type of test to write, or how to tell if a test is actually a good one.</p>

<p>Today let’s take a closer look at this topic. We’ll go over what TDD is, the principles behind a clean test (FIRST), the different types of automated tests (unit, integration, and end-to-end), and how all of that connects to the design of the code itself, not just to the guarantee that it works.</p>

<p>Oh, and it’s worth repeating: even though I’ll use examples with Ruby, Rails, RSpec, and Capybara, the ideas apply to any language or framework.</p>

<h2 id="tdd-test-driven-development">TDD (Test-Driven Development)</h2>

<p>TDD (Test-Driven Development) is a software construction technique.</p>

<p>It’s pretty common for people to think TDD is just a way of writing tests before the code, but it’s a lot more than that. TDD directly influences design, because the end result needs to be testable from the start.</p>

<p>Robert C. Martin, in Clean Code, sums up TDD in three laws:</p>

<ol>
  <li>You can’t write any production code before you have a test that detects a failure that doesn’t exist yet. In other words, no implementation starts without a test first proving that behavior isn’t there yet.</li>
  <li>You can’t write more of a unit test than what’s enough to detect that failure. Basically, the test needs to be small enough to prove just that, without already testing behaviors that haven’t even been implemented yet.</li>
  <li>You can’t write more production code than what’s enough to make that test pass. In other words, resist the urge to implement more than what the test is asking for at that moment.</li>
</ol>

<p>It’s worth clarifying here that the “production code” he keeps mentioning is the application code itself, the code that’s actually going to run and solve the problem. Think of it as the opposite of test code, which only exists to check whether the first one is behaving the way it should.</p>

<p>Anyway, following these laws to the letter, development happens in very small cycles, known as Red, Green, Refactor:</p>

<ul>
  <li><strong>Red</strong>: write a test that fails, because the behavior doesn’t exist yet.</li>
  <li><strong>Green</strong>: write the minimum amount of code to make that test pass, even if the implementation isn’t ideal yet.</li>
  <li><strong>Refactor</strong>: improve the code, knowing the tests that already pass keep guaranteeing nothing broke.</li>
</ul>

<p>The cycle repeats for the next behavior, and so on.</p>

<p>See why summing up TDD as just “writing the test first” oversimplifies things?</p>

<p>What TDD actually proposes is letting the need to test guide small design decisions, one behavior at a time.</p>

<h2 id="clean-tests-and-the-first-principles">Clean tests and the FIRST principles</h2>

<p>Writing the test before the code doesn’t guarantee that test is any good.</p>

<p>That’s where the FIRST principles come in, also presented by Robert Martin.</p>

<p>FIRST is an acronym:</p>

<p><strong>Fast</strong>: tests need to run quickly, because they’re going to be run constantly during development. And that’s not just about comfort, think about it, if the suite takes too long, people on the team end up running tests less often, or simply avoid writing new ones, which defeats the whole point of having automated tests in the first place.</p>

<p>Now, what counts as fast obviously changes depending on the level of the test. Unit tests tend to be a lot faster than integration tests, which in turn tend to be faster than end-to-end ones.</p>

<p><strong>Independent</strong>: a test shouldn’t depend on another one running, or on state left behind by a previous test.</p>

<p>A classic example of this is a test that only passes because a record was created by the previous test, without creating its own. In other words, if you run that test on its own, or change the order of the suite, it breaks, even with no real change in behavior. That’s a sign the tests are <a href="/en/what-is-coupling">coupled</a> in a way they shouldn’t be, each test needs to set up its own scenario from scratch.</p>

<p><strong>Repeatable</strong>: the result needs to be the same no matter how many times the test runs, whether on your machine, in CI, or a month from now.</p>

<p>A common example is a test that checks whether a promotion is active by comparing it to <code class="language-plaintext highlighter-rouge">Time.now</code>, something like <code class="language-plaintext highlighter-rouge">promotion.active?(Time.now)</code>. If the promotion’s expiration date used in the test is fixed, that test passes today and starts failing on its own once that date passes, even if nobody changed anything in the code.</p>

<p>On top of that, depending on an internet connection or a real external service also compromises repeatability, because the test starts failing due to network instability, not an actual problem in the application.</p>

<p><strong>Self-validating</strong>: the test itself needs to decide, on its own, whether it passed or failed, through a clear expectation, like <code class="language-plaintext highlighter-rouge">expect(result).to eq(expected)</code>. You shouldn’t need to read a log or manually check some output in the terminal.</p>

<p><strong>Timely</strong>: the test needs to be written at the right moment in development, not weeks after the code already exists. In TDD, that moment is before the production code.</p>

<p>Now that we understand what tests need to look like, let’s understand the types of tests we can have.</p>

<h2 id="types-of-tests">Types of tests</h2>

<p>There are three main types of automated tests: unit, integration, and end-to-end. Each one represents a different level of confidence and isolation.</p>

<p>The difference between them lies in what the test actually exercises.</p>

<p>What defines a test’s type is:</p>

<ul>
  <li>how much of the application it goes through</li>
  <li>how many real components take part</li>
  <li>whether there’s access to a database, network, files, or external services</li>
  <li>what input is used to exercise the system</li>
</ul>

<p>A question that helps a lot here is: <strong>what behavior am I testing, and which real parts does this test depend on?</strong></p>

<h3 id="unit-tests">Unit tests</h3>

<p>A unit test checks a small, specific part of the system, which can be a function, a method, a class, or a small object responsible for a rule.</p>

<p>A card number validator written as a simple Ruby class is a good example:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">CardNumberValidator</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">valid?</span><span class="p">(</span><span class="n">card_number</span><span class="p">)</span>
    <span class="c1"># Luhn's algorithm</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And its test:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">RSpec</span><span class="p">.</span><span class="nf">describe</span> <span class="no">CardNumberValidator</span> <span class="k">do</span>
  <span class="n">describe</span> <span class="s2">".valid?"</span> <span class="k">do</span>
    <span class="n">it</span> <span class="s2">"returns true for a valid card number"</span> <span class="k">do</span>
      <span class="n">expect</span><span class="p">(</span><span class="n">described_class</span><span class="p">.</span><span class="nf">valid?</span><span class="p">(</span><span class="s2">"4111111111111111"</span><span class="p">)).</span><span class="nf">to</span> <span class="n">be</span><span class="p">(</span><span class="kp">true</span><span class="p">)</span>
    <span class="k">end</span>

    <span class="n">it</span> <span class="s2">"returns false for an invalid card number"</span> <span class="k">do</span>
      <span class="n">expect</span><span class="p">(</span><span class="n">described_class</span><span class="p">.</span><span class="nf">valid?</span><span class="p">(</span><span class="s2">"1111111111111111"</span><span class="p">)).</span><span class="nf">to</span> <span class="n">be</span><span class="p">(</span><span class="kp">false</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">described_class</code> represents the class named in the outermost <code class="language-plaintext highlighter-rouge">describe</code>, in this case <code class="language-plaintext highlighter-rouge">CardNumberValidator</code>.</p>

<p>See how simple this test is: it doesn’t need to spin up a server, make an HTTP request, or save anything to the database. That’s why it tends to be fast, and when it fails, it’s easy to pinpoint the behavior that broke.</p>

<p>Also, a unit isn’t necessarily “a class”. Think of it more as a behavior boundary chosen for the test, which often maps to a class, but doesn’t have to.</p>

<p>There are also two common ways to think about a unit test: <strong>isolated</strong> (or solitary) and <strong>sociable</strong>.</p>

<p>The difference is in how each one deals with the collaborators of the object under test, that is, the other objects it uses internally to do its own work.</p>

<p>An isolated test replaces those collaborators with <em>test doubles</em>. We all know that in movies, a stunt double steps in for the actor in a scene, but still delivers what the scene needs.</p>

<p>In programming it’s the same idea: a double stands in for a real collaborator, controlled by the test, instead of running the actual logic behind it.</p>

<p>There are a few types of doubles, depending on what they do. The two most common are the <strong>stub</strong>, which just returns a predefined response when called, and the <strong>mock</strong>, where the test checks whether that collaborator was called the way it was expected to. We’ll see examples of both throughout the post.</p>

<p>So, if <code class="language-plaintext highlighter-rouge">CardNumberValidator</code> depended on another object to, say, strip spaces from the number before validating it:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">CardNumberValidator</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="ss">formatter: </span><span class="no">CardNumberFormatter</span><span class="p">.</span><span class="nf">new</span><span class="p">)</span>
    <span class="vi">@formatter</span> <span class="o">=</span> <span class="n">formatter</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">valid?</span><span class="p">(</span><span class="n">card_number</span><span class="p">)</span>
    <span class="n">luhn_valid?</span><span class="p">(</span><span class="vi">@formatter</span><span class="p">.</span><span class="nf">strip</span><span class="p">(</span><span class="n">card_number</span><span class="p">))</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>an isolated test would swap <code class="language-plaintext highlighter-rouge">CardNumberFormatter</code> for a stub, making sure only the validation logic is being exercised:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">formatter</span> <span class="o">=</span> <span class="n">instance_double</span><span class="p">(</span><span class="no">CardNumberFormatter</span><span class="p">)</span>
<span class="n">allow</span><span class="p">(</span><span class="n">formatter</span><span class="p">).</span><span class="nf">to</span> <span class="n">receive</span><span class="p">(</span><span class="ss">:strip</span><span class="p">).</span><span class="nf">with</span><span class="p">(</span><span class="s2">"4111 1111 1111 1111"</span><span class="p">).</span><span class="nf">and_return</span><span class="p">(</span><span class="s2">"4111111111111111"</span><span class="p">)</span>

<span class="n">validator</span> <span class="o">=</span> <span class="no">CardNumberValidator</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">formatter: </span><span class="n">formatter</span><span class="p">)</span>

<span class="n">expect</span><span class="p">(</span><span class="n">validator</span><span class="p">.</span><span class="nf">valid?</span><span class="p">(</span><span class="s2">"4111 1111 1111 1111"</span><span class="p">)).</span><span class="nf">to</span> <span class="n">be</span><span class="p">(</span><span class="kp">true</span><span class="p">)</span>
</code></pre></div></div>

<p>Notice the test doesn’t know (and doesn’t need to know) how <code class="language-plaintext highlighter-rouge">CardNumberFormatter</code> actually strips the spaces, it just defines what it expects to get back and checks whether the validator reacts to it correctly.</p>

<p>A sociable test, on the other hand, lets those real collaborators come into play. A validation declared with <code class="language-plaintext highlighter-rouge">validates</code> on a Rails model is a good example: the test calls <code class="language-plaintext highlighter-rouge">valid?</code> for real, letting the Active Record validation machinery run underneath, without swapping anything for a double. Even without touching the database, it’s still treated as a unit test, just a sociable one.</p>

<p>In large applications, this is how most unit tests show up day to day, through validators, calculators, policies, and other small domain objects, each one concentrating a specific rule without depending on much else around it.</p>

<p>Rails, though, has so much built in by convention that it can sometimes be hard to tell where the unit ends and the integration begins.</p>

<p>For example, a validation declared on the model runs inside <code class="language-plaintext highlighter-rouge">valid?</code> or <code class="language-plaintext highlighter-rouge">save</code>, along with the whole Active Record machinery behind it. That’s still a unit test, but the boundary is a lot less obvious than when the rule lives isolated in a simple Ruby class, like the <code class="language-plaintext highlighter-rouge">CardNumberValidator</code> we just saw.</p>

<p>In the end, the classification depends on what’s actually executed, not on where the file lives, or on the metadata RSpec Rails uses to identify the test, like <code class="language-plaintext highlighter-rouge">type: :model</code>.</p>

<h3 id="integration-tests">Integration tests</h3>

<p>An integration test checks whether different parts of the application, usually spread across different layers, like route, controller, model, and database, can correctly collaborate.</p>

<p>That already marks a big difference from a unit test. A unit test asks whether an isolated rule is correct. An integration test, on the other hand, asks whether several real pieces, together, produce the expected behavior, which usually involves more layers, and more points of failure, at the same time.</p>

<p>In Rails, request specs are the most common example:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">RSpec</span><span class="p">.</span><span class="nf">describe</span> <span class="s2">"POST /orders/checkout"</span><span class="p">,</span> <span class="ss">type: :request</span> <span class="k">do</span>
  <span class="n">it</span> <span class="s2">"creates an order"</span> <span class="k">do</span>
    <span class="n">input</span> <span class="o">=</span> <span class="p">{</span>
      <span class="ss">name: </span><span class="s2">"John Doe"</span><span class="p">,</span>
      <span class="ss">email: </span><span class="s2">"john.doe@gmail.com"</span><span class="p">,</span>
      <span class="ss">card_number: </span><span class="s2">"4111111111111111"</span>
    <span class="p">}</span>

    <span class="n">post</span> <span class="s2">"/orders/checkout"</span><span class="p">,</span> <span class="ss">params: </span><span class="n">input</span><span class="p">,</span> <span class="ss">as: :json</span>

    <span class="n">output</span> <span class="o">=</span> <span class="n">response</span><span class="p">.</span><span class="nf">parsed_body</span>

    <span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_http_status</span><span class="p">(</span><span class="ss">:created</span><span class="p">)</span>
    <span class="n">expect</span><span class="p">(</span><span class="n">output</span><span class="p">[</span><span class="s2">"order_id"</span><span class="p">]).</span><span class="nf">not_to</span> <span class="n">be_nil</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Notice this test goes through the route, controller, parameters, <code class="language-plaintext highlighter-rouge">Order</code>, validations, Active Record, the database, and the JSON response. It’s not just asking whether the card number is valid, the question is bigger: <strong>do the parts needed to complete an order work together?</strong></p>

<p>There’s a technical detail worth mentioning here.</p>

<p>Even when running <code class="language-plaintext highlighter-rouge">post "/orders/checkout"</code>, RSpec Rails normally sends that request to the application through Rack, inside the test process itself. It doesn’t need to spin up a server on a port, or make a real network call. That’s why request specs tend to be classified as integration tests for the application, not as end-to-end.</p>

<p>In a more minimalist framework, like Sinatra, the equivalent usually uses <code class="language-plaintext highlighter-rouge">Rack::Test</code> directly:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">RSpec</span><span class="p">.</span><span class="nf">describe</span> <span class="s2">"POST /orders/checkout"</span> <span class="k">do</span>
  <span class="kp">include</span> <span class="no">Rack</span><span class="o">::</span><span class="no">Test</span><span class="o">::</span><span class="no">Methods</span>

  <span class="k">def</span> <span class="nf">app</span>
    <span class="no">App</span>
  <span class="k">end</span>

  <span class="n">it</span> <span class="s2">"creates an order"</span> <span class="k">do</span>
    <span class="n">post</span> <span class="s2">"/orders/checkout"</span><span class="p">,</span>
         <span class="no">JSON</span><span class="p">.</span><span class="nf">generate</span><span class="p">(</span><span class="n">input</span><span class="p">),</span>
         <span class="p">{</span> <span class="s2">"CONTENT_TYPE"</span> <span class="o">=&gt;</span> <span class="s2">"application/json"</span> <span class="p">}</span>

    <span class="n">expect</span><span class="p">(</span><span class="n">last_response</span><span class="p">.</span><span class="nf">status</span><span class="p">).</span><span class="nf">to</span> <span class="n">eq</span><span class="p">(</span><span class="mi">201</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="end-to-end-tests-e2e">End-to-end tests (E2E)</h3>

<p>An end-to-end test goes through the system starting from an externally used interface, simulating a complete user flow. In a Rails web application, this is usually done with a system spec and Capybara:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">RSpec</span><span class="p">.</span><span class="nf">describe</span> <span class="s2">"Order checkout"</span><span class="p">,</span> <span class="ss">type: :system</span> <span class="k">do</span>
  <span class="n">it</span> <span class="s2">"allows a customer to complete an order"</span> <span class="k">do</span>
    <span class="n">visit</span> <span class="s2">"/checkout"</span>

    <span class="n">fill_in</span> <span class="s2">"Name"</span><span class="p">,</span> <span class="ss">with: </span><span class="s2">"John Doe"</span>
    <span class="n">fill_in</span> <span class="s2">"Email"</span><span class="p">,</span> <span class="ss">with: </span><span class="s2">"john.doe@gmail.com"</span>
    <span class="n">fill_in</span> <span class="s2">"Card number"</span><span class="p">,</span> <span class="ss">with: </span><span class="s2">"4111111111111111"</span>

    <span class="n">click_button</span> <span class="s2">"Complete order"</span>

    <span class="n">expect</span><span class="p">(</span><span class="n">page</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_content</span><span class="p">(</span><span class="s2">"Order confirmed"</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This flow goes through the browser, page, form, JavaScript, server, controller, model, database, and the response shown back on the interface.</p>

<p>Capybara isn’t the browser itself, it offers a language for interacting with web applications, and uses a driver underneath, for example:</p>

<ul>
  <li><strong>Rack::Test</strong>: fast, runs in-process, doesn’t support JavaScript;</li>
  <li><strong>Selenium with Chrome</strong>: uses a real or headless browser, supports JavaScript;</li>
  <li><strong>Cuprite</strong>: controls headless Chrome.</li>
</ul>

<p>A system spec with a real browser gets closer to a complete end-to-end test. A Capybara test with <code class="language-plaintext highlighter-rouge">Rack::Test</code>, on the other hand, simulates navigation, but doesn’t use a real browser.</p>

<p>In APIs, that boundary is less agreed upon. Some people call a full endpoint test end-to-end, because it goes from the request all the way to the database. In common Rails vocabulary, though, an in-process request spec is usually treated as integration anyway. A stricter end-to-end test would boot the whole application and make a real HTTP request against an actual server.</p>

<p>End-to-end tests tend to be expensive to maintain and more prone to breaking for reasons that have nothing to do with the behavior being tested, like a layout change on the screen. That’s why there tend to be a lot fewer of these than the other two types.</p>

<h2 id="how-much-of-each-type-of-test-to-write">How much of each type of test to write</h2>

<p>Because of that cost, and the instability that comes with it, in practice we don’t apply the three types of tests with the same intensity. And there’s no fixed ratio between them, it varies from application to application, depending on the risk of each part of the system and how critical each user journey is.</p>

<p>A simple idea helps decide that: each level covers a different set of cases, without repeating what the other one already guarantees.</p>

<p>Let’s go back to the card number validation:</p>

<p>The <strong>validator’s unit test</strong> covers the algorithm’s scenarios in detail: numbers that pass Luhn validation, invalid check digit, all matching digits, <code class="language-plaintext highlighter-rouge">nil</code>, values that are too short, values that are too long, with spaces or dashes.</p>

<p>The <strong>checkout’s integration test</strong> just needs to confirm that rule is wired up to the endpoint, in other words, a POST with an invalid card number returns 422 and the right error message.</p>

<p>We don’t need to repeat every invalid number already covered by the unit test, right?</p>

<p>The <strong>end-to-end test</strong>, if there’s an interface, covers the main flow: the customer fills out the form and the order goes through. Running every algorithm case through the browser would make the suite slow and redundant without adding any extra confidence.</p>

<p>This distribution is usually described as a pyramid: lots of unit tests for rules with several combinations, request specs covering the important contracts of the endpoints, and few system specs for the most critical user journeys.</p>

<p>An example of how this usually gets organized in a Rails application, without being a fixed formula:</p>

<table>
  <thead>
    <tr>
      <th>Level</th>
      <th>Examples</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Unit</td>
      <td>validators, calculators, policies, domain objects</td>
    </tr>
    <tr>
      <td>Model</td>
      <td>validations, model methods and rules</td>
    </tr>
    <tr>
      <td>Integration</td>
      <td>request specs, persistence, jobs with database access</td>
    </tr>
    <tr>
      <td>End-to-end</td>
      <td>system specs with Capybara for critical flows</td>
    </tr>
  </tbody>
</table>

<p>And it’s worth remembering we don’t need to test absolutely everything. Most applications don’t even come close to 100% coverage, and that’s fine. What actually deserves attention are the critical parts, the ones that, if they break, affect users drastically.</p>

<h2 id="testability-as-a-consequence-of-design">Testability as a consequence of design</h2>

<p>Remember I mentioned TDD influences design? That happens because writing the test first forces you to think about how that code is going to be called before thinking about how it’s going to be implemented. That relationship works both ways: code that’s hard to test is usually pointing at a design problem, not just a lack of tests.</p>

<h3 id="explicit-dependencies">Explicit dependencies</h3>

<p>A <a href="/en/dependency-management">dependency</a> is anything a piece of code needs in order to do its own job, whether that’s another object, an external service, or the database. It’s explicit when it shows up visibly in the code itself, usually as a parameter or argument, and it’s implicit when the code simply assumes that thing is going to be available, without declaring it anywhere.</p>

<p>The more implicit a dependency is, the harder it gets to test that piece of code in isolation, because the test first needs to figure out what needs to exist behind the scenes for that behavior to work.</p>

<p>More minimalist frameworks, like Sinatra, keep these dependencies clearly visible right in the route:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">post</span> <span class="s2">"/orders/checkout"</span> <span class="k">do</span>
  <span class="n">input</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="nf">body</span><span class="p">.</span><span class="nf">read</span><span class="p">)</span>

  <span class="n">halt</span> <span class="mi">422</span> <span class="k">unless</span> <span class="no">NameValidator</span><span class="p">.</span><span class="nf">valid?</span><span class="p">(</span><span class="n">input</span><span class="p">[</span><span class="s2">"name"</span><span class="p">])</span>
  <span class="n">halt</span> <span class="mi">422</span> <span class="k">unless</span> <span class="no">CardNumberValidator</span><span class="p">.</span><span class="nf">valid?</span><span class="p">(</span><span class="n">input</span><span class="p">[</span><span class="s2">"card_number"</span><span class="p">])</span>

  <span class="no">Order</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="n">input</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Each dependency shows up explicitly, right there by name: the request, the name validator, the card validator, <code class="language-plaintext highlighter-rouge">Order</code>, the database.</p>

<p>In Rails, the same rule can just be declared on the model:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">validates</span> <span class="ss">:card_number</span><span class="p">,</span> <span class="ss">card_number: </span><span class="kp">true</span>
</code></pre></div></div>

<p>and get executed implicitly by <code class="language-plaintext highlighter-rouge">order.save</code>, which can trigger validations, custom validators, callbacks, and persistence in a single call. That cuts down on repetition, but it also hides part of the flow behind convention.</p>

<p>Testing that behavior requires knowing what Rails is doing underneath, not just what’s written right there in front of you.</p>

<p>Neither approach is wrong, but they call for different kinds of tests to reach the same level of confidence: in Sinatra, testing each validator in isolation already makes it clear what’s happening; in Rails, besides the isolated validator, it’s worth adding an integration test confirming the convention is actually wiring the pieces together.</p>

<h3 id="injected-dependencies-and-test-doubles">Injected dependencies and <em>test doubles</em></h3>

<p>Imagine a use case that sends a confirmation email after an order is placed:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">CreateOrder</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">email_sender</span><span class="p">:)</span>
    <span class="vi">@email_sender</span> <span class="o">=</span> <span class="n">email_sender</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">input</span><span class="p">)</span>
    <span class="n">order</span> <span class="o">=</span> <span class="no">Order</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="n">input</span><span class="p">)</span>
    <span class="vi">@email_sender</span><span class="p">.</span><span class="nf">send_order_confirmation</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
    <span class="n">order</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">CreateOrder</code> receives <code class="language-plaintext highlighter-rouge">email_sender</code> from the outside, instead of instantiating a concrete service internally. That’s what we call dependency injection, and it’s what makes this test possible without sending a real email:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">email_sender</span> <span class="o">=</span> <span class="n">instance_double</span><span class="p">(</span><span class="no">EmailSender</span><span class="p">)</span>

<span class="n">allow</span><span class="p">(</span><span class="n">email_sender</span><span class="p">).</span><span class="nf">to</span> <span class="n">receive</span><span class="p">(</span><span class="ss">:send_order_confirmation</span><span class="p">)</span>

<span class="n">service</span> <span class="o">=</span> <span class="no">CreateOrder</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">email_sender: </span><span class="n">email_sender</span><span class="p">)</span>
<span class="n">order</span> <span class="o">=</span> <span class="n">service</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">input</span><span class="p">)</span>

<span class="n">expect</span><span class="p">(</span><span class="n">email_sender</span><span class="p">)</span>
  <span class="p">.</span><span class="nf">to</span> <span class="n">have_received</span><span class="p">(</span><span class="ss">:send_order_confirmation</span><span class="p">)</span>
  <span class="p">.</span><span class="nf">with</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">email_sender</code> here is the same kind of double we saw earlier, just working as a mock this time instead of a stub, since the check isn’t about a return value, it’s about the <code class="language-plaintext highlighter-rouge">send_order_confirmation</code> call actually happening.</p>

<p>This mock works well because <code class="language-plaintext highlighter-rouge">CreateOrder</code> only depends on one thing outside its main behavior, sending the email. If testing <code class="language-plaintext highlighter-rouge">CreateOrder</code> required mocking several unrelated dependencies just to reach a single expectation, that would be a sign the class has <a href="/en/single-responsibility">too much responsibility</a>, or that its <a href="/en/dependency-management">dependency management</a> needs some adjustment.</p>

<p>Keep in mind that a test that’s hard to write is almost always pointing at a design problem, not a lack of testing tools. That’s the thread connecting everything we’ve seen so far.</p>

<p>TDD guides the construction process, FIRST guides the quality of each individual test, and testability works as feedback on the design itself: a dependency that’s hard to isolate, spread-out responsibility, or excessive coupling all show up first in how hard a test is to write, long before they turn into a problem in production.</p>

<p>Within that process, unit, integration, and end-to-end tests represent different levels of confidence and isolation, each one answering a different question about the system.</p>

<p>In Rails, these boundaries look less clear, because the framework connects route, controller, model, validation, and persistence by convention. That’s why it’s always worth remembering: the folder name shows where a test is organized, but the behavior and dependencies it exercises are what actually show its real level.</p>

<p>I hope this helped make the topic a little clearer.</p>

<p>See you next time!</p>]]></content><author><name>Aline Marques</name></author><category term="Software Architecture" /><category term="Ruby on Rails" /><summary type="html"><![CDATA[Understand the difference between unit, integration, and end-to-end tests, how TDD influences code design, the FIRST principles, and how testability connects to architecture decisions in Ruby on Rails applications.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://alinemarques.dev/assets/images/logo.png" /><media:content medium="image" url="https://alinemarques.dev/assets/images/logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="en"><title type="html">Dependency Management: what one part of a system needs to know about another</title><link href="https://alinemarques.dev/en/dependency-management" rel="alternate" type="text/html" title="Dependency Management: what one part of a system needs to know about another" /><published>2026-08-15T00:00:00-03:00</published><updated>2026-08-15T00:00:00-03:00</updated><id>https://alinemarques.dev/gerenciamento-dependencias.en</id><content type="html" xml:base="https://alinemarques.dev/dependency-management"><![CDATA[<p>You’ve probably seen plenty of collaboration between components and parts of an application’s code. As a rule, every application is made of parts that need to collaborate. One component fetches information that belongs to another, executes a behavior that isn’t its own, or calls an external integration to finish its own job.</p>

<p>That’s what we call a dependency.</p>

<p>A common reaction, especially when you’re starting to study software design, is to think dependency is synonymous with problem, and that the goal is to eliminate as many of them as possible.</p>

<p>But it’s not quite like that. Sandi Metz, in <em>Practical Object-Oriented Design in Ruby</em>, treats managing dependencies as one of the core skills of object-oriented design, not as something to avoid at all costs, especially since in a lot of cases the dependency can be unavoidable.</p>

<p>So today let’s understand what a dependency actually is, why it isn’t always a problem, and how to think about it both in an isolated class and in a whole application, with multiple domains, services, and integrations.</p>

<h2 id="what-is-a-dependency">What is a dependency?</h2>

<p>Imagine an <code class="language-plaintext highlighter-rouge">Enrollment</code> class, from an online course platform, that needs to know how much of the course the student has already completed:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Enrollment</span>
  <span class="k">def</span> <span class="nf">progress</span>
    <span class="no">CourseModule</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">course</span><span class="p">,</span> <span class="n">student</span><span class="p">).</span><span class="nf">completion_percentage</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>At first glance, it looks like there’s just one dependency: <code class="language-plaintext highlighter-rouge">Enrollment</code> depends on <code class="language-plaintext highlighter-rouge">CourseModule</code>. But looking more closely, <code class="language-plaintext highlighter-rouge">Enrollment</code> knows a lot more than that:</p>

<ul>
  <li>that there’s a class called <code class="language-plaintext highlighter-rouge">CourseModule</code>;</li>
  <li>what arguments it expects, <code class="language-plaintext highlighter-rouge">course</code> and <code class="language-plaintext highlighter-rouge">student</code>, in that order;</li>
  <li>that the object it creates responds to the method <code class="language-plaintext highlighter-rouge">completion_percentage</code>.</li>
</ul>

<p>This already shows an important idea: a dependency can be understood as the knowledge one part of a system needs to have about another. The more details a class knows about another, the higher the <a href="/en/what-is-coupling">coupling</a> between them tends to be.</p>

<p>And that knowledge doesn’t just come from creating the object. Even if <code class="language-plaintext highlighter-rouge">Enrollment</code> received the module already built, as a parameter, it would still depend on something pretty specific:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">course_module</span><span class="p">.</span><span class="nf">completion_percentage</span>
</code></pre></div></div>

<p>In other words, it would still depend on that object responding to the method <code class="language-plaintext highlighter-rouge">completion_percentage</code>, even without knowing how that calculation happens internally.</p>

<p>This is what we call <strong>interface dependency</strong>, which is when you don’t depend on how the other component solves its own problem, you only depend on the behavior it exposes outward.</p>

<p>So, we can sum it up like this: <strong>a dependency exists when a component needs something provided by another component in order to do its own job.</strong></p>

<h2 id="dependency-between-domains-and-modules">Dependency between domains and modules</h2>

<p>Dependency examples usually show up at the class and object level, but the same reasoning applies at a much bigger scale, between whole domains and modules of an application (which is something we see a lot more in day to day work as devs).</p>

<p>Imagine that same course platform is organized into modules, and that the <code class="language-plaintext highlighter-rouge">Certificates</code> module needs to issue a certificate to the student as soon as they finish a course:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">Certificates</span>
  <span class="k">class</span> <span class="nc">IssueCertificate</span>
    <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">student_id</span><span class="p">,</span> <span class="n">course_id</span><span class="p">)</span>
      <span class="n">student</span> <span class="o">=</span> <span class="no">Students</span><span class="o">::</span><span class="no">Student</span><span class="p">.</span><span class="nf">active</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">student_id</span><span class="p">)</span>
      <span class="c1"># ...</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">IssueCertificate</code> is a class that does exactly what its name says: issues a certificate. It’s a common Rails pattern, a class with a single public method, usually called <code class="language-plaintext highlighter-rouge">call</code>, dedicated to executing a specific action.</p>

<p>Notice that <code class="language-plaintext highlighter-rouge">IssueCertificate</code> doesn’t just know a student exists, it knows the <code class="language-plaintext highlighter-rouge">Students::Student</code> model, the <code class="language-plaintext highlighter-rouge">active</code> scope, the <code class="language-plaintext highlighter-rouge">find</code> method, and possibly the database structure behind all of it.</p>

<p>On top of that, if <code class="language-plaintext highlighter-rouge">Students</code> changes something internally, for example swapping <code class="language-plaintext highlighter-rouge">active</code> for more elaborate status logic, <code class="language-plaintext highlighter-rouge">IssueCertificate</code> risks breaking right along with it, even though it’s from a completely different domain.</p>

<p>One alternative is for <code class="language-plaintext highlighter-rouge">IssueCertificate</code> to talk to a public boundary of <code class="language-plaintext highlighter-rouge">Students</code>, like an API, instead of the internal model:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">Certificates</span>
  <span class="k">class</span> <span class="nc">IssueCertificate</span>
    <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">student_id</span><span class="p">,</span> <span class="n">course_id</span><span class="p">)</span>
      <span class="n">student</span> <span class="o">=</span> <span class="no">Students</span><span class="o">::</span><span class="no">Public</span><span class="o">::</span><span class="no">Api</span><span class="p">.</span><span class="nf">find_student</span><span class="p">(</span><span class="n">student_id</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Notice the dependency didn’t disappear, <code class="language-plaintext highlighter-rouge">Certificates</code> still needs information that belongs to <code class="language-plaintext highlighter-rouge">Students</code>. But now there’s a controlled boundary between the two domains.</p>

<p>This is pretty similar to the idea of contracts between layers we saw in the post about <a href="/en/layered-architecture">layered architecture</a>, just applied between domains instead of between presentation, business, and data.</p>

<p>And here’s a caveat worth mentioning, a public API isn’t automatically an abstraction. <code class="language-plaintext highlighter-rouge">Students::Public::Api</code> is still a concrete module, it just protects the internal details of <code class="language-plaintext highlighter-rouge">Students::Student</code>.</p>

<p>But don’t worry, we’ll come back to that difference later.</p>

<p>Either way, this already makes a central idea clear: <strong>managing dependencies doesn’t mean stopping domains from talking to each other. It means controlling what each one needs to know about the other.</strong></p>

<h2 id="direction-of-the-dependency">Direction of the dependency</h2>

<p>One thing to keep in mind from the start is that every dependency has a direction.</p>

<p><code class="language-plaintext highlighter-rouge">Certificates</code> depending on <code class="language-plaintext highlighter-rouge">Students</code> is a different decision than <code class="language-plaintext highlighter-rouge">Students</code> depending on <code class="language-plaintext highlighter-rouge">Certificates</code>.</p>

<p>And the question that matters here is: who should know about whom?</p>

<p>There’s no generic answer, because it depends on responsibility, stability, and how much a change on one side should (or shouldn’t) affect the other.</p>

<p>Let’s think about a different situation.</p>

<p>It would be strange for the <code class="language-plaintext highlighter-rouge">Students</code> module to know details about <code class="language-plaintext highlighter-rouge">Certificates</code> just to be able to list which courses a student has already completed. That would flip the natural relationship between the two domains, <code class="language-plaintext highlighter-rouge">Students</code> would end up knowing about certification just to return information that, in practice, belongs more naturally to itself.</p>

<p>Flipping a dependency doesn’t automatically make the design better, it just changes who depends on whom, and sometimes puts the responsibility somewhere that doesn’t make sense.</p>

<p>A good rule for choosing the direction is to look at how likely each side is to change.</p>

<p>In general, more concrete, domain-specific classes tend to change more than stable interfaces.</p>

<p>And just to clarify, a concrete class is a class with a specific implementation, a defined way of solving a problem, unlike an interface, which only defines the expected behavior without committing to how it’s solved internally (this is the same idea of interface dependency we saw at the start of the post).</p>

<p>A good example of this is <code class="language-plaintext highlighter-rouge">Students::Public::Api</code> itself, which we saw above.</p>

<p><code class="language-plaintext highlighter-rouge">IssueCertificate</code> depends on the method <code class="language-plaintext highlighter-rouge">find_student(student_id)</code>, that’s the interface, the contract it knows about. How that method solves the problem internally is the concrete part:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">Students</span>
  <span class="k">module</span> <span class="nn">Public</span>
    <span class="k">module</span> <span class="nn">Api</span>
      <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">find_student</span><span class="p">(</span><span class="n">student_id</span><span class="p">)</span>
        <span class="no">Student</span><span class="p">.</span><span class="nf">active</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">student_id</span><span class="p">)</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now imagine that tomorrow the <code class="language-plaintext highlighter-rouge">Students</code> team decides to look up the student in a cache first, and only hit the database if it’s not found:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">Students</span>
  <span class="k">module</span> <span class="nn">Public</span>
    <span class="k">module</span> <span class="nn">Api</span>
      <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">find_student</span><span class="p">(</span><span class="n">student_id</span><span class="p">)</span>
        <span class="no">Rails</span><span class="p">.</span><span class="nf">cache</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s2">"student:</span><span class="si">#{</span><span class="n">student_id</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span> <span class="p">{</span> <span class="no">Student</span><span class="p">.</span><span class="nf">active</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">student_id</span><span class="p">)</span> <span class="p">}</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The implementation changed completely, but the interface, the method name, the argument it expects, and what it returns, stays exactly the same.</p>

<p><code class="language-plaintext highlighter-rouge">IssueCertificate</code> doesn’t even find out something changed internally. That’s why interfaces tend to be more stable than concrete implementations. What changes most often is what’s inside, not the contract exposed to the outside.</p>

<p>And it’s not just within our own application that this applies.</p>

<p>We depend on <code class="language-plaintext highlighter-rouge">String</code>, <code class="language-plaintext highlighter-rouge">Array</code>, <code class="language-plaintext highlighter-rouge">Enumerable</code> all the time in the same way, without worrying much about it, because their interface practically never changes. An internal class built to solve a specific business rule, like <code class="language-plaintext highlighter-rouge">IssueCertificate</code>, is a lot more likely to have both its interface and its implementation change tomorrow.</p>

<p>So, whenever possible, it’s worth making dependencies point in the direction of what’s more stable.</p>

<h2 id="when-a-dependency-starts-to-become-a-problem">When a dependency starts to become a problem</h2>

<p>Not every dependency carries the same risk. Two questions help identify when a dependency starts to weigh things down: how often it changes, and how many parts depend on it.</p>

<p>Imagine <code class="language-plaintext highlighter-rouge">Student#full_name</code> is used by several different modules, <code class="language-plaintext highlighter-rouge">Certificates</code>, <code class="language-plaintext highlighter-rouge">Notifications</code>, and <code class="language-plaintext highlighter-rouge">Reports</code>, all calling that same method:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Certificates  ─┐
Notifications ─┼──→ Student#full_name
Reports       ─┘
</code></pre></div></div>

<p>If that interface changes, for example if it starts requiring a new argument, all three modules feel the impact at the same time.</p>

<p>So, the more things depend on an interface, the more stable it needs to be, because the cost of changing it grows along with the number of dependents.</p>

<p>The riskiest scenario is exactly the combination of both things: something that changes frequently and, at the same time, has a lot of dependents.</p>

<p>If <code class="language-plaintext highlighter-rouge">Students::Student</code> were being accessed directly (without the boundary we built above) by several different modules, any adjustment to <code class="language-plaintext highlighter-rouge">Student</code>’s structure would turn into a risk event for the whole application.</p>

<h2 id="how-to-reduce-knowledge-between-components">How to reduce knowledge between components</h2>

<p>After recognizing a dependency and evaluating how much it weighs, the next step is deciding what to do about it. There are a few techniques that help reduce the knowledge one component needs to have about another.</p>

<h3 id="injecting-dependencies">Injecting dependencies</h3>

<p>Without injection, <code class="language-plaintext highlighter-rouge">IssueCertificate</code> builds what it needs on its own:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">IssueCertificate</span>
  <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">student</span><span class="p">,</span> <span class="n">course</span><span class="p">)</span>
    <span class="no">PdfGeneratorWrapper</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">generate</span><span class="p">(</span><span class="n">student</span><span class="p">,</span> <span class="n">course</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>It knows which generator to use, how to create an instance of it, and that it responds to <code class="language-plaintext highlighter-rouge">generate</code>.</p>

<p>With dependency injection, that creation happens outside instead:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">IssueCertificate</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">generator</span><span class="p">:)</span>
    <span class="vi">@generator</span> <span class="o">=</span> <span class="n">generator</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">student</span><span class="p">,</span> <span class="n">course</span><span class="p">)</span>
    <span class="vi">@generator</span><span class="p">.</span><span class="nf">generate</span><span class="p">(</span><span class="n">student</span><span class="p">,</span> <span class="n">course</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">IssueCertificate</span>
  <span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">generator: </span><span class="no">PdfGeneratorWrapper</span><span class="p">.</span><span class="nf">new</span><span class="p">)</span>
  <span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">student</span><span class="p">,</span> <span class="n">course</span><span class="p">)</span>
</code></pre></div></div>

<p>Dependency injection is basically that, <strong>an object receives from the outside what it needs to work</strong>, instead of building that dependency on its own.</p>

<p>This doesn’t eliminate the dependency, <code class="language-plaintext highlighter-rouge">IssueCertificate</code> still needs something that responds to <code class="language-plaintext highlighter-rouge">generate(student, course)</code>. What changes is the type of knowledge. Before, it knew which concrete class to use and how to create it. Now, it only knows which behavior it expects to receive.</p>

<p>Much better, right?</p>

<p>This also makes testing a lot easier, because you can inject a fake generator without touching <code class="language-plaintext highlighter-rouge">IssueCertificate</code> at all.</p>

<h3 id="isolating-dependencies">Isolating dependencies</h3>

<p>Isolating a dependency means concentrating the knowledge about it in a single place. The dependency is still there, the gain is avoiding that knowledge being spread across several points of the application.</p>

<p>And believe me, in very large applications, isolation is essential.</p>

<p>Going back to the <code class="language-plaintext highlighter-rouge">Enrollment</code> example from the start, imagine <code class="language-plaintext highlighter-rouge">progress</code> also needs to apply a weight:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">progress</span>
  <span class="n">weight</span> <span class="o">*</span> <span class="no">CourseModule</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">course</span><span class="p">,</span> <span class="n">student</span><span class="p">).</span><span class="nf">completion_percentage</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Creating the <code class="language-plaintext highlighter-rouge">CourseModule</code> is mixed together with the calculation. But we can separate it:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">progress</span>
  <span class="n">weight</span> <span class="o">*</span> <span class="n">course_module</span><span class="p">.</span><span class="nf">completion_percentage</span>
<span class="k">end</span>

<span class="k">def</span> <span class="nf">course_module</span>
  <span class="vi">@course_module</span> <span class="o">||=</span> <span class="no">CourseModule</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">course</span><span class="p">,</span> <span class="n">student</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now <code class="language-plaintext highlighter-rouge">progress</code> only knows how to use the module, and <code class="language-plaintext highlighter-rouge">course_module</code> knows how to build the object.</p>

<p>The <code class="language-plaintext highlighter-rouge">||=</code> here guarantees the object is only created the first time <code class="language-plaintext highlighter-rouge">course_module</code> is called, and reused after that, which is usually called <em>lazy initialization</em>, where creation is delayed until the moment it’s actually needed.</p>

<p>We can also isolate the method call itself, not just the creation. <code class="language-plaintext highlighter-rouge">progress</code> still knows <code class="language-plaintext highlighter-rouge">course_module</code> responds to <code class="language-plaintext highlighter-rouge">completion_percentage</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">progress</span>
  <span class="n">weight</span> <span class="o">*</span> <span class="n">completion_percentage</span>
<span class="k">end</span>

<span class="k">def</span> <span class="nf">completion_percentage</span>
  <span class="n">course_module</span><span class="p">.</span><span class="nf">completion_percentage</span>
<span class="k">end</span>
</code></pre></div></div>

<p>That specific knowledge stays concentrated inside <code class="language-plaintext highlighter-rouge">completion_percentage</code>. If tomorrow the progress calculation comes from somewhere else, only that method needs to change.</p>

<p>This same principle shows up when the application depends on an external library we don’t control. Imagine the certificate’s PDF generation uses an external gem, with an interface based on positional arguments:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">ExternalPdfLib</span><span class="o">::</span><span class="no">Generator</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">student</span><span class="p">.</span><span class="nf">name</span><span class="p">,</span> <span class="n">course</span><span class="p">.</span><span class="nf">title</span><span class="p">,</span> <span class="no">Time</span><span class="p">.</span><span class="nf">current</span><span class="p">)</span>
</code></pre></div></div>

<p>Since we don’t control that interface, creating a <em>wrapper</em> (a layer that wraps this external dependency and hides its details from the rest of the application) helps concentrate that knowledge:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">PdfGeneratorWrapper</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">generate</span><span class="p">(</span><span class="n">student</span><span class="p">:,</span> <span class="n">course</span><span class="p">:)</span>
    <span class="no">ExternalPdfLib</span><span class="o">::</span><span class="no">Generator</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">student</span><span class="p">.</span><span class="nf">name</span><span class="p">,</span> <span class="n">course</span><span class="p">.</span><span class="nf">title</span><span class="p">,</span> <span class="no">Time</span><span class="p">.</span><span class="nf">current</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The rest of the application now calls <code class="language-plaintext highlighter-rouge">PdfGeneratorWrapper.generate(student: student, course: course)</code>, without needing to know the exact order of arguments the gem expects.</p>

<p>Only the wrapper knows about that peculiarity.</p>

<p>Isolating this kind of logic in a dedicated object, similar to a <a href="/en/service-objects-rails">service object</a>, is a common way to contain that knowledge in one place.</p>

<p>And here’s a caveat worth mentioning, it doesn’t make sense to create a wrapper for every gem or every external call in the application. The abstraction needs to solve a real problem, not exist just for the sake of existing.</p>

<h3 id="depending-on-abstractions-not-on-concrete-implementations">Depending on abstractions, not on concrete implementations</h3>

<p>In the dependency injection example, <code class="language-plaintext highlighter-rouge">IssueCertificate</code> calls <code class="language-plaintext highlighter-rouge">@generator.generate(student, course)</code>.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">IssueCertificate</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">generator</span><span class="p">:)</span>
    <span class="vi">@generator</span> <span class="o">=</span> <span class="n">generator</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">student</span><span class="p">,</span> <span class="n">course</span><span class="p">)</span>
    <span class="vi">@generator</span><span class="p">.</span><span class="nf">generate</span><span class="p">(</span><span class="n">student</span><span class="p">,</span> <span class="n">course</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>That call represents an abstraction, because it only expresses the behavior <code class="language-plaintext highlighter-rouge">IssueCertificate</code> needs, without determining how it’s going to be carried out.</p>

<p><code class="language-plaintext highlighter-rouge">PdfGeneratorWrapper</code>, which we just saw, is a concrete implementation of that behavior, but it isn’t the only possible one.</p>

<p>In a test, for example, we can create another implementation, a fake generator that just returns a fixed PDF, without calling the real gem:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">FakePdfGenerator</span>
  <span class="k">def</span> <span class="nf">generate</span><span class="p">(</span><span class="n">student</span><span class="p">,</span> <span class="n">course</span><span class="p">)</span>
    <span class="s2">"Fake PDF for </span><span class="si">#{</span><span class="n">student</span><span class="p">.</span><span class="nf">name</span><span class="si">}</span><span class="s2">"</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And when writing the test, it’s this fake implementation that takes the place of the real one:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">IssueCertificate</span>
  <span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">generator: </span><span class="no">FakePdfGenerator</span><span class="p">.</span><span class="nf">new</span><span class="p">)</span>
  <span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">student</span><span class="p">,</span> <span class="n">course</span><span class="p">)</span>
</code></pre></div></div>

<p>Notice that <code class="language-plaintext highlighter-rouge">PdfGeneratorWrapper</code> and <code class="language-plaintext highlighter-rouge">FakePdfGenerator</code> fulfill the same contract, both respond to <code class="language-plaintext highlighter-rouge">generate(student, course)</code>. That’s why <code class="language-plaintext highlighter-rouge">IssueCertificate</code> works the same way in both cases: it only knows the abstraction, the expected behavior, it doesn’t know (and doesn’t need to know) which of the two implementations it’s receiving.</p>

<p>In Ruby, we don’t normally declare this interface explicitly, the way we would in a statically typed language. The expectation that the object responds to <code class="language-plaintext highlighter-rouge">generate</code> shows up implicitly, through the methods we call on it. This is usually called <em>duck typing</em>, what matters is that the object responds to the expected method, not which class it belongs to.</p>

<p>In <em>Clean Architecture</em>, Robert C. Martin describes something similar through the <em>Dependency Inversion Principle</em>, depending on the behavior you need, not on the specific implementation that currently provides it. It’s the same idea as Sandi Metz’s, just described at a scale that already thinks in terms of whole components, not just objects.</p>

<p>And it’s worth reinforcing that earlier caveat, <code class="language-plaintext highlighter-rouge">Students::Public::Api</code> creates a boundary, but it’s still a concrete implementation, a specific module with a specific method. Boundary and abstraction are similar concepts, but they aren’t the same thing.</p>

<h2 id="not-every-dependency-needs-to-be-removed">Not every dependency needs to be removed</h2>

<p>A common mistake is treating dependency as synonymous with problem. Every application depends on Ruby, on Rails, on gems, on other domains, on internal and external services. That’s unavoidable, and that’s fine.</p>

<p>The question that actually matters isn’t whether an arrow <code class="language-plaintext highlighter-rouge">A → B</code> exists. It’s what kind of knowledge crosses that arrow.</p>

<p>A simple, stable, intentional dependency can be perfectly acceptable without any special technique applied to it.</p>

<p>It’s also worth telling dependency and coupling apart here.</p>

<p>A dependency is the fact that one component needs another to work. Coupling, on the other hand, describes how tightly those parts are tied together, and how much changes in one affect the other.</p>

<p>They’re related concepts, but they aren’t synonyms.</p>

<h2 id="how-to-evaluate-a-dependency-day-to-day">How to evaluate a dependency day to day</h2>

<p>Alright, now that we’ve talked through all of this, when you run into a new dependency, or review an existing one, a few questions help decide what to do with it.</p>

<p>The first one is: <strong>what exactly does this code depend on?</strong></p>

<p>A specific implementation, a model, a public API, a behavior contract?</p>

<p>Not every dependency calls for the same kind of care.</p>

<p>Another useful question: <strong>how much does this component know about the other one?</strong></p>

<p>Does it only know the behavior it needs, or does it also know internal details, like database structure, queries, and payload format?</p>

<p>It’s also worth considering: <strong>how often does this dependency change, and how many parts depend on it?</strong></p>

<p>A combination of frequent change with a lot of dependents is a sign that point deserves extra attention.</p>

<p>And, before going and creating abstractions for everything, <strong>does this abstraction solve a real problem?</strong></p>

<p>Not every dependency needs to turn into an interface, gateway, adapter, or wrapper. Adding a layer where it isn’t necessary just adds complexity without reducing any risk.</p>

<p>At the end of the day, what we can take away from today isn’t ways to eliminate dependencies. It’s understanding that they’re unavoidable, and that the amount of knowledge crossing these relationships is a design decision, not an accident.</p>

<p>An application that’s easier to change is, generally, an application where each component only knows what it actually needs to know about the others.</p>

<p>If you want to go deeper on this topic, the two books that guided this post are great starting points: <em>Practical Object-Oriented Design in Ruby</em>, by Sandi Metz, and <em>Clean Architecture</em>, by Robert C. Martin.</p>

<p>See you next time!</p>]]></content><author><name>Aline Marques</name></author><category term="Software Architecture" /><category term="Ruby on Rails" /><summary type="html"><![CDATA[Understand what dependencies between components are, why they aren't a problem on their own, and how techniques like dependency injection, isolation, and abstraction help control the knowledge shared between classes and domains.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://alinemarques.dev/assets/images/logo.png" /><media:content medium="image" url="https://alinemarques.dev/assets/images/logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="en"><title type="html">Single Responsibility (SRP): thinking beyond classes</title><link href="https://alinemarques.dev/en/single-responsibility" rel="alternate" type="text/html" title="Single Responsibility (SRP): thinking beyond classes" /><published>2026-07-19T00:00:00-03:00</published><updated>2026-07-19T00:00:00-03:00</updated><id>https://alinemarques.dev/single-responsibility.en</id><content type="html" xml:base="https://alinemarques.dev/single-responsibility"><![CDATA[<p>Today let’s talk about Single Responsibility. If you’ve heard of SOLID, you’ve definitely run into this principle before, it’s the “S” in the acronym.</p>

<p>It’s one of the best known principles when it comes to code design. Sandi Metz dedicates a whole chapter to it in Practical Object-Oriented Design, and Robert C. Martin talks about it a lot in both Clean Code and Clean Architecture.</p>

<p>Even though it’s a well known principle, it gets a lot trickier when you try to apply it outside the classic single-class example.</p>

<p>So let’s understand what it is, see how it shows up in a class, and then expand that thinking to a bigger application context, where it’s not just classes, there’s services, modules, jobs, lots of things happening at the same time.</p>

<h2 id="what-is-single-responsibility">What is Single Responsibility?</h2>

<p>According to Sandi Metz, <strong>a class should do the smallest useful thing possible</strong>. In other words, it should have a single responsibility.</p>

<p>The most famous phrase about this, from Robert C. Martin, is: a class should have one, and only one, reason to change.</p>

<p>The definition doesn’t say “do only one thing”, it says “reason to change”. That difference matters a lot, and we’ll come back to it later.</p>

<p>Let’s look at an example. Imagine an <code class="language-plaintext highlighter-rouge">Order</code> class in an e-commerce app:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Order</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="k">def</span> <span class="nf">total</span>
    <span class="n">items</span><span class="p">.</span><span class="nf">sum</span> <span class="p">{</span> <span class="o">|</span><span class="n">item</span><span class="o">|</span> <span class="n">item</span><span class="p">.</span><span class="nf">price</span> <span class="o">*</span> <span class="n">item</span><span class="p">.</span><span class="nf">quantity</span> <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">apply_discount</span><span class="p">(</span><span class="n">coupon</span><span class="p">)</span>
    <span class="n">total</span> <span class="o">-</span> <span class="n">coupon</span><span class="p">.</span><span class="nf">value</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">send_confirmation_email</span>
    <span class="no">OrderMailer</span><span class="p">.</span><span class="nf">confirmation</span><span class="p">(</span><span class="nb">self</span><span class="p">).</span><span class="nf">deliver_now</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This class calculates the order total, applies a discount, and also sends an email.</p>

<p>So there are three different reasons for this class to change:</p>
<ul>
  <li>the total calculation rule</li>
  <li>the discount rule</li>
  <li>the way the confirmation is sent</li>
</ul>

<p>If any one of these three things changes, <code class="language-plaintext highlighter-rouge">Order</code> changes too, even if the other two stay exactly the same.</p>

<p>We can split it like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Order</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">OrderPricer</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">total</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
    <span class="n">order</span><span class="p">.</span><span class="nf">items</span><span class="p">.</span><span class="nf">sum</span> <span class="p">{</span> <span class="o">|</span><span class="n">item</span><span class="o">|</span> <span class="n">item</span><span class="p">.</span><span class="nf">price</span> <span class="o">*</span> <span class="n">item</span><span class="p">.</span><span class="nf">quantity</span> <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">DiscountCalculator</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">apply</span><span class="p">(</span><span class="n">order</span><span class="p">,</span> <span class="n">coupon</span><span class="p">)</span>
    <span class="no">OrderPricer</span><span class="p">.</span><span class="nf">total</span><span class="p">(</span><span class="n">order</span><span class="p">)</span> <span class="o">-</span> <span class="n">coupon</span><span class="p">.</span><span class="nf">value</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">OrderConfirmationMailer</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">send</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
    <span class="no">OrderMailer</span><span class="p">.</span><span class="nf">confirmation</span><span class="p">(</span><span class="n">order</span><span class="p">).</span><span class="nf">deliver_now</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now each class has only one reason to change. If the discount rule changes, only <code class="language-plaintext highlighter-rouge">DiscountCalculator</code> is touched. <code class="language-plaintext highlighter-rouge">Order</code> stays the same.</p>

<p>You might be thinking “wait, isn’t this creating too many classes?”</p>

<p>That’s a pretty common concern. Robert C. Martin himself talks about this fear, of creating too many small classes and the code ending up too spread out to follow.</p>

<p>And from my point of view, it’s a trade-off. Either you simplify thinking about future maintenance, or you keep everything together out of fear of “creating too much code” and pay that price down the road, when you need to change one rule without affecting the others.</p>

<p>So yeah, splitting things up too much can feel like overkill, especially at the start and in small projects. But in large applications, with a lot of people working on the same code, this separation tends to pay off quite a bit later on.</p>

<p>And deep down, it’s a choice, there’s no absolute right or wrong. Sandi Metz herself talks about this in the book:</p>

<blockquote>
  <p><em>A good designer understands this tension and minimizes costs by making informed tradeoffs between the needs of the present and the possibilities of the future.</em></p>
</blockquote>

<p>It’s also worth quickly talking about some good practices Sandi Metz brings up in the book, that help the code keep respecting SRP as it evolves, without needing to refactor everything every time a rule changes.</p>

<p>One point she raises is <strong>avoiding accessing instance variables directly inside the class itself</strong>. It seems trivial at first, but using <code class="language-plaintext highlighter-rouge">@type</code> and <code class="language-plaintext highlighter-rouge">@value</code> directly across several different methods spreads the knowledge of how that data is stored throughout the whole class:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Coupon</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">type</span><span class="p">)</span>
    <span class="vi">@value</span> <span class="o">=</span> <span class="n">value</span>
    <span class="vi">@type</span> <span class="o">=</span> <span class="n">type</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">apply</span><span class="p">(</span><span class="n">total</span><span class="p">)</span>
    <span class="vi">@type</span> <span class="o">==</span> <span class="ss">:percentage</span> <span class="p">?</span> <span class="n">total</span> <span class="o">-</span> <span class="p">(</span><span class="n">total</span> <span class="o">*</span> <span class="vi">@value</span> <span class="o">/</span> <span class="mf">100.0</span><span class="p">)</span> <span class="p">:</span> <span class="n">total</span> <span class="o">-</span> <span class="vi">@value</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Hiding these variables behind methods, any new rule about them stays concentrated in one place:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Coupon</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">type</span><span class="p">)</span>
    <span class="vi">@value</span> <span class="o">=</span> <span class="n">value</span>
    <span class="vi">@type</span> <span class="o">=</span> <span class="n">type</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">apply</span><span class="p">(</span><span class="n">total</span><span class="p">)</span>
    <span class="n">percentage?</span> <span class="p">?</span> <span class="n">total</span> <span class="o">-</span> <span class="p">(</span><span class="n">total</span> <span class="o">*</span> <span class="n">value</span> <span class="o">/</span> <span class="mf">100.0</span><span class="p">)</span> <span class="p">:</span> <span class="n">total</span> <span class="o">-</span> <span class="n">value</span>
  <span class="k">end</span>

  <span class="kp">private</span>

  <span class="nb">attr_reader</span> <span class="ss">:value</span><span class="p">,</span> <span class="ss">:type</span>

  <span class="k">def</span> <span class="nf">percentage?</span>
    <span class="n">type</span> <span class="o">==</span> <span class="ss">:percentage</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>If the rule for “what counts as a percentage coupon” changes, only <code class="language-plaintext highlighter-rouge">percentage?</code> needs to change.</p>

<p>She also talks about a similar problem with raw data structures, like arrays and hashes. <strong>If the code depends on a position in an array or a key in a hash to work, any change to that structure breaks everything that depends on it</strong>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">total</span><span class="p">(</span><span class="n">line_items</span><span class="p">)</span>
  <span class="n">line_items</span><span class="p">.</span><span class="nf">sum</span> <span class="p">{</span> <span class="o">|</span><span class="n">item</span><span class="o">|</span> <span class="n">item</span><span class="p">[</span><span class="ss">:price</span><span class="p">]</span> <span class="o">*</span> <span class="n">item</span><span class="p">[</span><span class="ss">:quantity</span><span class="p">]</span> <span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div>

<p>If someone renames the <code class="language-plaintext highlighter-rouge">:price</code> key to <code class="language-plaintext highlighter-rouge">:unit_price</code>, this method breaks, and it’s probably not the only place accessing that hash this way. Encapsulating this in an object fixes it:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">LineItem</span> <span class="o">=</span> <span class="no">Struct</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:price</span><span class="p">,</span> <span class="ss">:quantity</span><span class="p">)</span> <span class="k">do</span>
  <span class="k">def</span> <span class="nf">subtotal</span>
    <span class="n">price</span> <span class="o">*</span> <span class="n">quantity</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">def</span> <span class="nf">total</span><span class="p">(</span><span class="n">line_items</span><span class="p">)</span>
  <span class="n">line_items</span><span class="p">.</span><span class="nf">sum</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:subtotal</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Whoever calls <code class="language-plaintext highlighter-rouge">total</code> doesn’t need to know how a <code class="language-plaintext highlighter-rouge">LineItem</code> stores its data internally, just that it responds to <code class="language-plaintext highlighter-rouge">subtotal</code>.</p>

<p>And finally, this same reasoning also applies inside methods, not just between classes.</p>

<p>A method that validates, calculates, and formats at the same time has, in practice, the same reasons to change that a poorly split class would have, just on a smaller scale:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">apply</span><span class="p">(</span><span class="n">total</span><span class="p">)</span>
  <span class="k">raise</span> <span class="no">ArgumentError</span><span class="p">,</span> <span class="s2">"invalid coupon"</span> <span class="k">if</span> <span class="n">value</span><span class="p">.</span><span class="nf">negative?</span>
  <span class="n">percentage?</span> <span class="p">?</span> <span class="n">total</span> <span class="o">-</span> <span class="p">(</span><span class="n">total</span> <span class="o">*</span> <span class="n">value</span> <span class="o">/</span> <span class="mf">100.0</span><span class="p">)</span> <span class="p">:</span> <span class="n">total</span> <span class="o">-</span> <span class="n">value</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Separating validation from calculation fixes this the same way:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">apply</span><span class="p">(</span><span class="n">total</span><span class="p">)</span>
  <span class="n">validate!</span>
  <span class="n">percentage?</span> <span class="p">?</span> <span class="n">total</span> <span class="o">-</span> <span class="p">(</span><span class="n">total</span> <span class="o">*</span> <span class="n">value</span> <span class="o">/</span> <span class="mf">100.0</span><span class="p">)</span> <span class="p">:</span> <span class="n">total</span> <span class="o">-</span> <span class="n">value</span>
<span class="k">end</span>

<span class="kp">private</span>

<span class="k">def</span> <span class="nf">validate!</span>
  <span class="k">raise</span> <span class="no">ArgumentError</span><span class="p">,</span> <span class="s2">"invalid coupon"</span> <span class="k">if</span> <span class="n">value</span><span class="p">.</span><span class="nf">negative?</span>
<span class="k">end</span>
</code></pre></div></div>

<p>So far, this is the most common example we see when SRP comes up. But what happens when we step outside the class level?</p>

<h2 id="single-responsibility-beyond-classes">Single Responsibility beyond classes</h2>

<p>We’ve talked about SRP in a relatively isolated class. In this scenario, we just need to ask ourselves which behaviors really belong to that object.</p>

<p>It’s the kind of reasoning Sandi Metz proposes when “interrogating” a class. If a method seems to answer a question that shouldn’t be asked of that object, there might be a responsibility in the wrong place.</p>

<p>In a real application, though, there are a lot more candidates to take on a responsibility. A rule can live in an entity, a value object, a use case, a policy, a job, an adapter, or some other component of the application.</p>

<p>Because of that, the question stops being just “does this class do more than one thing?” and becomes “who should know about or execute this behavior?”.</p>

<p>Let’s go back to the same <code class="language-plaintext highlighter-rouge">Order</code> we used above. Questions about its state, its items, or its total value are still the order’s own problem, that doesn’t change:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">order</span><span class="p">.</span><span class="nf">total</span>
<span class="n">order</span><span class="p">.</span><span class="nf">issued?</span>
<span class="n">order</span><span class="p">.</span><span class="nf">line_items</span>
</code></pre></div></div>

<p>But not everything that mentions “order” is <code class="language-plaintext highlighter-rouge">Order</code>’s responsibility.</p>

<p>Sending the confirmation email, charging the customer, or coordinating the whole checkout flow are different responsibilities, each with its own reason to change. They can live in a mailer, a payment gateway client, or a use case that orchestrates the whole process:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">OrderConfirmationMailer</span><span class="p">.</span><span class="nf">send</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
<span class="no">PaymentGatewayClient</span><span class="p">.</span><span class="nf">charge</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
<span class="no">SubmitOrder</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
</code></pre></div></div>

<p>SRP is still the same principle as always. What changes in a larger application is the number of possible places for that responsibility to live, and that’s what makes it harder to see.</p>

<p>Another one of Robert C. Martin’s definitions helps a lot here. According to him, a “reason to change” is tied to a person or group of people interested in that change, what he calls an actor.</p>

<p>This gets easier to see when we look at a whole component, not just loose methods. Imagine a component called <code class="language-plaintext highlighter-rouge">OrdersService</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">OrdersService</span>
  <span class="k">def</span> <span class="nf">process</span><span class="p">(</span><span class="n">order_params</span><span class="p">)</span>
    <span class="no">Order</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="n">order_params</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">monthly_report</span>
    <span class="no">Order</span>
      <span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="s2">"created_at &gt;= ?"</span><span class="p">,</span> <span class="mi">30</span><span class="p">.</span><span class="nf">days</span><span class="p">.</span><span class="nf">ago</span><span class="p">)</span>
      <span class="p">.</span><span class="nf">group</span><span class="p">(</span><span class="ss">:status</span><span class="p">)</span>
      <span class="p">.</span><span class="nf">count</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">notify_marketing</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
    <span class="no">MarketingNotifier</span><span class="p">.</span><span class="nf">order_confirmed</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>At first glance, the methods look related because they all mention orders. But they represent different knowledge:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">process</code> knows the operational rules for creating orders</li>
  <li><code class="language-plaintext highlighter-rouge">monthly_report</code> knows analytics and reporting needs</li>
  <li><code class="language-plaintext highlighter-rouge">notify_marketing</code> knows the marketing communication process</li>
</ul>

<p>Doing three things isn’t really the problem. The problem is that these three parts can change independently, for different reasons.</p>

<p>A new rule for creating orders shouldn’t force the same component to change along with report generation. In the same way, a change in marketing communication shouldn’t put order processing at risk.</p>

<p>We can separate these responsibilities:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ProcessOrder</span>
  <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">order_params</span><span class="p">)</span>
    <span class="no">Order</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="n">order_params</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">MonthlySalesReport</span>
  <span class="k">def</span> <span class="nf">generate</span>
    <span class="no">Order</span>
      <span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="s2">"created_at &gt;= ?"</span><span class="p">,</span> <span class="mi">30</span><span class="p">.</span><span class="nf">days</span><span class="p">.</span><span class="nf">ago</span><span class="p">)</span>
      <span class="p">.</span><span class="nf">group</span><span class="p">(</span><span class="ss">:status</span><span class="p">)</span>
      <span class="p">.</span><span class="nf">count</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">NotifyMarketingAboutOrder</span>
  <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
    <span class="no">MarketingNotifier</span><span class="p">.</span><span class="nf">order_confirmed</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The exact names don’t matter, and neither does the fact that we ended up with three classes. What matters is that each component now concentrates knowledge that tends to change together.</p>

<p>Deep down, <code class="language-plaintext highlighter-rouge">OrdersService</code> is still just a class. That’s how we always express any unit in Ruby, whether it’s a domain, a module, or a whole service, the syntax doesn’t change. What changes is what that component represents inside the application, and how many different actors depend on it.</p>

<p>This same logic shows up in units we don’t usually think of as a “class”, like a job. Imagine a job that closes out an order’s checkout:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">CheckoutJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
  <span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
    <span class="n">order</span><span class="p">.</span><span class="nf">update!</span><span class="p">(</span><span class="ss">status: :paid</span><span class="p">)</span>
    <span class="no">InvoiceGenerator</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
    <span class="no">LoyaltyPointsCalculator</span><span class="p">.</span><span class="nf">credit</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
    <span class="no">MarketingNotifier</span><span class="p">.</span><span class="nf">order_confirmed</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The job doesn’t know the internal rules of each step, <code class="language-plaintext highlighter-rouge">LoyaltyPointsCalculator</code> handles points, <code class="language-plaintext highlighter-rouge">InvoiceGenerator</code> handles the invoice. What <code class="language-plaintext highlighter-rouge">CheckoutJob</code> concentrates is the orchestration: deciding which steps happen, in what order, and what to do when one of them fails.</p>

<p>And that’s where the problem lies, the four steps end up sharing the same fate, if any of them fails, ActiveJob retries the whole job again. A failure in <code class="language-plaintext highlighter-rouge">MarketingNotifier</code>, for example, makes the job reprocess the payment and generate the invoice again, even if those two steps had already succeeded on the first attempt.</p>

<p>If the finance team wants invoice generation to have its own retry policy, without re-running payment and loyalty along with it, the whole <code class="language-plaintext highlighter-rouge">CheckoutJob</code> needs to change, because the orchestration of the four steps lives in one single place.</p>

<p>Here, the reason to change is the coordination between teams, not the knowledge of each one’s rules. Same problem as <code class="language-plaintext highlighter-rouge">OrdersService</code>, just that the unit is now a job.</p>

<p>We can fix this by separating orchestration from execution. Instead of the job calling the four steps in sequence, sharing the same fate, each step becomes its own job, triggered independently.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">CheckoutJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
  <span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
    <span class="n">order</span><span class="p">.</span><span class="nf">update!</span><span class="p">(</span><span class="ss">status: :paid</span><span class="p">)</span>

    <span class="no">GenerateInvoiceJob</span><span class="p">.</span><span class="nf">perform_later</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
    <span class="no">CreditLoyaltyPointsJob</span><span class="p">.</span><span class="nf">perform_later</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
    <span class="no">NotifyMarketingJob</span><span class="p">.</span><span class="nf">perform_later</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">GenerateInvoiceJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
  <span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
    <span class="no">InvoiceGenerator</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">CreditLoyaltyPointsJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
  <span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
    <span class="no">LoyaltyPointsCalculator</span><span class="p">.</span><span class="nf">credit</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">NotifyMarketingJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
  <span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
    <span class="no">MarketingNotifier</span><span class="p">.</span><span class="nf">order_confirmed</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now <code class="language-plaintext highlighter-rouge">CheckoutJob</code> has just one responsibility: confirming the payment and triggering the other steps, without waiting for or depending on their result. Each queued step runs independently, if <code class="language-plaintext highlighter-rouge">NotifyMarketingJob</code> fails, only it gets reprocessed by ActiveJob. Payment and invoice aren’t touched again.</p>

<p>And each job now has just one reason to change, matching a single actor, <code class="language-plaintext highlighter-rouge">GenerateInvoiceJob</code> changes when the tax rule changes, and the finance team decides its own retry policy. <code class="language-plaintext highlighter-rouge">CreditLoyaltyPointsJob</code> changes when the loyalty rule changes. <code class="language-plaintext highlighter-rouge">NotifyMarketingJob</code> changes when marketing communication changes.</p>

<p>Before, these four reasons to change were tied together in the same <code class="language-plaintext highlighter-rouge">perform</code>, sharing the same retry policy without anyone having decided that on purpose.</p>

<p>The separation doesn’t reduce code, it decouples the fate of each responsibility from the others. It’s the same logic as <code class="language-plaintext highlighter-rouge">Order</code> at the start of the post, just applied at the level of asynchronous execution.</p>

<p>If you’ve already read the post about <a href="/en/what-is-coupling">coupling</a> here on the blog, this is pretty similar to that idea that a design concept isn’t tied to a single form, it repeats itself across different units of the application: class, service, job, module.</p>

<p>At the end of the day, SRP isn’t a rule exclusive to models or small classes. It’s a way of thinking about distributing responsibility: group together what changes together, separate what changes for different reasons, whether that’s a class, a service, or a job.</p>

<p>In a larger application, the principle still holds. It just gets harder to see, because now there are more units, layers, and integrations competing for space for each responsibility.</p>

<h2 id="how-to-think-about-this-day-to-day">How to think about this day to day</h2>

<p>Thinking about the Single Responsibility principle day to day isn’t always easy (I say that from experience). So a question that helps a lot, at any level, whether it’s a class, service, or module, is: <strong>who’s going to ask for a change here, and why?</strong></p>

<p>If the answer involves more than one team, or more than one reason that has nothing to do with the other, that’s a sign that component is carrying more than one responsibility.</p>

<p>Another tip, this one from Sandi Metz herself, is <strong>trying to describe what that component does in a single sentence</strong>. If you need to use “and” or “or” along the way, like “processes the order and sends a marketing notification”, that’s already a sign there’s more than one responsibility in there.</p>

<p>It’s also worth <strong>paying attention to who touches that file, and why</strong>. If the commit history shows changes from completely different teams, one day it’s the finance team adjusting the invoice, another day it’s marketing changing the email copy, that’s a pretty concrete sign that different actors live in there.</p>

<p>And <strong>notice how much effort it takes to test</strong>. If testing a component requires mocking a bunch of unrelated things, like a payment gateway, a points calculator, and a marketing client in the same test, that’s usually a reflection of mixed responsibilities. A hard to write test almost always points to a design problem.</p>

<p>And remember, just like with coupling, the goal isn’t to take this to the extreme. It’s not possible, and it doesn’t make sense, to split everything into tiny components just for the sake of it. The idea is to notice where different reasons to change are mixed together in the same place, and then decide whether it’s worth separating them.</p>

<p>This is one of those principles that seems simple when you only look at the single-class example, but that gains another layer of complexity as the application grows and more people start working on it.</p>

<p>I hope this post helped you think about SRP in a slightly broader way.</p>

<p>See you next time!</p>]]></content><author><name>Aline Marques</name></author><category term="Software Architecture" /><category term="Ruby on Rails" /><summary type="html"><![CDATA[Understand the Single Responsibility Principle (SRP), how it works in classes, and how to think about it in larger applications with multiple services.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://alinemarques.dev/assets/images/logo.png" /><media:content medium="image" url="https://alinemarques.dev/assets/images/logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="en"><title type="html">Tech interviews: stages and tips based on my experience</title><link href="https://alinemarques.dev/en/tech-interview-process" rel="alternate" type="text/html" title="Tech interviews: stages and tips based on my experience" /><published>2026-06-30T00:00:00-03:00</published><updated>2026-06-30T00:00:00-03:00</updated><id>https://alinemarques.dev/entrevistas.en</id><content type="html" xml:base="https://alinemarques.dev/tech-interview-process"><![CDATA[<p>One of the questions I hear the most these days, as a programming teacher, is: “how do I get into tech?”</p>

<p>There’s no exact answer to that question, but because of it, I decided to talk a bit about how my own experience with interviews has been.</p>

<p><strong>Before anything else</strong>: what I’m bringing here is based on my personal experience, okay? The scope is pretty specific. I’m talking about mid-level developer roles (and senior in some cases), with a Ruby on Rails, React, and JavaScript stack, at Brazilian and, mainly, international companies.</p>

<p>Unfortunately, I don’t have records of my junior interviews because, back then, I didn’t document anything. So this post focuses on what I’ve been through since then.</p>

<p>And of course, every company organizes the process differently. The point here isn’t to say your interview is going to look exactly like this, but to show some formats I’ve come across and things that can help with preparation.</p>

<p>That said, let’s get into it.</p>

<h2 id="what-do-these-processes-usually-look-like">What do these processes usually look like?</h2>

<p>Processes vary a lot. Some companies start with a recruiter, some put you straight into a conversation with the CTO. Some processes have five clearly defined stages, and some lay out a full roadmap and then change everything halfway through.</p>

<p>One thing that became pretty clear to me is that the name of a stage doesn’t always describe what’s going to happen. An “initial chat” can be purely informational, but it can also be the first, deeper evaluation of the process.</p>

<p>Because of that, before each interview, it’s worth trying to understand two things: what stage it is, and who you’re going to talk to.</p>

<h2 id="1-interview-with-a-recruiter">1. Interview with a recruiter</h2>

<p>Usually, the first stage of a hiring process is with someone from recruiting.</p>

<p><strong>Here’s a tip</strong>: before joining the call, it’s worth searching LinkedIn for the name that shows up on the meeting invite. In some cases, this first conversation can actually be directly with someone from engineering, and knowing that ahead of time changes a lot about how it makes sense to prepare.</p>

<p>In general, what usually comes up in a conversation with a recruiter is:</p>

<ul>
  <li>your career path</li>
  <li>the technologies you work with</li>
  <li>some relevant project or challenge</li>
  <li>your motivation for looking for a new role</li>
  <li>salary expectations</li>
</ul>

<p>That last one came up in almost every process I’ve been through, so it’s good to go in already knowing how you plan to answer. One thing I learned is that there’s no problem asking if the company has a salary range, in a lot of cases they’ll tell you. But when they don’t define one, you’ll have to name a number yourself.</p>

<p>If it’s someone from recruiting, you probably won’t need to go too deep into the technical side. What matters most tends to be explaining your experience clearly and connecting what you’ve already done with what the role is looking for.</p>

<p>Something that helped me a lot was rehearsing beforehand, especially for interviews in English. It gave me a lot more fluency when speaking and helped me stay focused on what I actually needed to show.</p>

<h2 id="2-interview-with-someone-from-engineering-or-leadership">2. Interview with someone from engineering or leadership</h2>

<p>In a lot of processes, it’s normal and expected to have a conversation with someone from engineering, a CTO, or some other leader. This can happen right in the first stage or at another point in the process. It really depends on the company.</p>

<p>And this conversation usually ends up being a lot more technical than the stage’s name makes it sound.</p>

<p>I’ve had a conversation that didn’t involve any code or exercise, but where I needed to explain a project in depth: what the problem was, what decisions were made, what trade-offs existed, and what I’d do differently today.</p>

<p>I’ve also been asked things like:</p>

<ul>
  <li>how I make technical decisions</li>
  <li>how I start working with an API I’ve never used</li>
  <li>how I handle finding a bug in production</li>
  <li>what I’d change in the system I currently work on</li>
  <li>how I’d defend a technical change to my manager</li>
  <li>how I act when a deadline gets tighter</li>
  <li>what I like and don’t like about the stack I use</li>
</ul>

<p>Among many others.</p>

<p>So it’s worth showing up prepared to talk not just about what you did, but why you did it that way.</p>

<p>Something I learned is that it helps a lot to have two or three projects you really know well. You don’t need to memorize a speech, but it’s good to be able to explain the context, your part in it, the decisions, the outcome, and what you learned.</p>

<h2 id="3-culture-fit--behavioral-interview">3. Culture fit / behavioral interview</h2>

<p>Some processes have a stage focused on culture and behavior, more about understanding how you work and seeing if it fits the way the company works.</p>

<p>If the company has a page about its values, it’s worth reading it before the interview. They’ll probably be looking for behaviors aligned with those values in what you say.</p>

<p>So if the company values things like independence and async communication, for example, it doesn’t look great if you show that you prefer daily check-ins or need a lot of validation from the team to make decisions.</p>

<p>And remember: at this stage, <strong>the focus is behavioral</strong>.</p>

<p>Once, I had an interview like this conducted by two engineers, and because they were engineers, I thought I needed to nail all the technical details.</p>

<p>I remember there was a question about how I deal with disagreement on a team, and I ended up talking about the system’s architecture. That wasn’t what they wanted to understand.</p>

<p>What mattered there was my attitude, how the situation got resolved, and what I learned. The technical part was just context.</p>

<p>The topics tend to circle around:</p>

<ul>
  <li>communication and collaboration</li>
  <li>how you deal with disagreement and feedback</li>
  <li>autonomy and decision making</li>
  <li>mistakes and learning</li>
  <li>remote work and async communication</li>
</ul>

<p>When we get nervous, the tendency is to stretch out answers and lose the thread. A structure that can help is thinking in terms of <strong>situation, action, result, and learning</strong>.</p>

<p>You don’t have to be rigid about it, but keeping that sequence in mind avoids answers that start in one place and end up somewhere else entirely.</p>

<p>And this stage can also tell you a lot about the company.</p>

<p>During these conversations, I’ve been able to get a better sense of how often meetings happen, how much autonomy the team has, and how decisions tend to get made. While you’re being evaluated, you’re also observing.</p>

<h2 id="4-technical-interview">4. Technical interview</h2>

<p>This was, by far, the stage with the most variation across the processes I’ve been through. It can be live coding, conceptual questions, a business rule, a take home exercise, an architecture diagram, or a mix of several of these. I’ll mostly talk about the formats I’ve actually experienced.</p>

<h3 id="live-coding">Live coding</h3>

<p>Live coding is the kind of interview where you need to code live, sharing your screen or using some online editor. It’s shown up for me as:</p>

<ul>
  <li>a logic exercise</li>
  <li>a business rule in JavaScript and Ruby</li>
  <li>a React implementation</li>
  <li>CRUD in Rails</li>
</ul>

<p>You can’t always know the format ahead of time, so it’s worth preparing both for your own environment and for using a more limited online editor.</p>

<p>What tends to get evaluated isn’t just whether you reach the right solution, but <strong>how you interpret the problem, how you react when something goes wrong, and how you communicate while solving it</strong>.</p>

<p>In one of the interviews I did, the exercise was to go through a string, record the position of each letter in a <a href="/en/ruby-hashes">hash</a>, and then sort the results.</p>

<p>I got to a working solution, but with a less elegant approach. Even so, I was able to move forward.</p>

<p>What stuck with me from that is that starting with something simple usually beats getting stuck waiting for the perfect solution to show up.</p>

<p>In another one, the exercise involved calculating how many weeks a certain amount of medication would last. The simple scenario was fine, but when the quantity didn’t divide evenly into whole weeks, things got complicated. The problem wasn’t the language, it was translating a math rule into code under pressure.</p>

<p>Both examples reminded me of something important: <strong>it’s always worth practicing logic</strong>. Being able to solve a problem from scratch, without AI, without Stack Overflow, is exactly what live coding is going to demand from you.</p>

<p>AI is great for day to day work, but in live coding you won’t have it by your side. If you’re used to relying on it to think, it can be tough when it’s not available. It’s worth training without it every once in a while, just to keep that muscle active.</p>

<p>A tip that makes a difference: <strong>talk out loud while you solve it</strong>.</p>

<p>Explain what you understood about the problem, what path you plan to follow, what you’re testing. Staying silent makes it harder to evaluate you, because the other person only sees the code and can’t follow your reasoning.</p>

<p>It feels weird at first, but it’s something worth practicing.</p>

<p>Oh, and some companies let you check documentation during the exercise. If that’s the case, great, because in day to day work nobody remembers everything off the top of their head.</p>

<h3 id="conceptual-questions-and-architecture-scenarios">Conceptual questions and architecture scenarios</h3>

<p>Besides live coding, you might get questions about concepts and technical situations:</p>

<ul>
  <li>Open-Closed Principle</li>
  <li><a href="/en/single-responsibility">separation of responsibilities</a></li>
  <li>background jobs</li>
  <li>feature flags</li>
  <li>tests</li>
  <li>databases</li>
  <li>cache</li>
  <li>migrations</li>
</ul>

<p>These questions aren’t always the “explain concept X” type. Sometimes the person lays out a situation and wants to understand how you think through building a solution.</p>

<p>In one interview, I was asked how I’d handle a migration on a table that was constantly receiving data and couldn’t be locked. The question wasn’t just about knowing how to add a column, it was about thinking through the old data, the new records coming in during the migration, temporary compatibility, and the risk of downtime.</p>

<p>Organizing your answer in stages, before, during, and after the change, helps a lot in these cases. And it’s fine to take a few seconds to organize your thoughts before you start talking.</p>

<h3 id="architecture-assessment">Architecture assessment</h3>

<p>Some companies ask for an assessment more focused on architecture, usually as a system design exercise during the interview or as an artifact sent beforehand, like a diagram of a real project, to discuss live afterward.</p>

<p>Unlike live coding, the focus isn’t on writing the implementation. <strong>The idea is to understand how you organize a bigger problem and what points you consider before proposing a solution</strong>.</p>

<p>When the company asks for an artifact beforehand, it’s worth asking: is it a pass or fail step, or just a starting point for a conversation? What scale are they expecting to see? Do they want the project you know best, or the biggest system you’ve worked on?</p>

<p>Things like that will help you figure out how to put the artifact together.</p>

<h2 id="what-the-interview-reveals-about-the-company">What the interview reveals about the company</h2>

<p>Something I picked up on over time is that the interview isn’t just a test for you to pass. It also tells you a lot about the company.</p>

<p>How the process is run, whether the criteria are clear or implicit, whether the stages are respected or change without notice, whether people show up prepared, all of that says a lot about how the company organizes itself and makes decisions day to day.</p>

<p>So while you’re being evaluated, it’s important to observe too. You don’t need to leave every conversation only thinking about whether the company liked you.</p>

<p>It’s also worth thinking about whether what you heard makes sense for the kind of place you’d like to work at. Especially since that’s the company where you’ll be spending a good chunk of your day.</p>

<p>Everything I brought up here is experience I still use to try to improve with every process. There’s no formula, but there is practice, self assessment, and, over time, more clarity about what worked and what didn’t work for you.</p>

<p>And that goes both ways: both to get better at interviewing and to understand if the place evaluating you is really the place where you want to be.</p>

<p>See you next time!</p>]]></content><author><name>Aline Marques</name></author><category term="Career" /><summary type="html"><![CDATA[What I learned going through interview processes for mid-level developer roles with Ruby on Rails, React, and JavaScript, at Brazilian and international companies.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://alinemarques.dev/assets/images/logo.png" /><media:content medium="image" url="https://alinemarques.dev/assets/images/logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="en"><title type="html">Layered Architecture</title><link href="https://alinemarques.dev/en/layered-architecture" rel="alternate" type="text/html" title="Layered Architecture" /><published>2026-06-14T00:00:00-03:00</published><updated>2026-06-14T00:00:00-03:00</updated><id>https://alinemarques.dev/arquitetura-camadas.en</id><content type="html" xml:base="https://alinemarques.dev/layered-architecture"><![CDATA[<p>If you’ve ever worked with Rails, you’ve probably used layered architecture without even noticing. It shows up, in one way or another, in most of the applications we build day to day.</p>

<p>But what exactly is it, and why does it matter? Let’s talk about it.</p>

<h2 id="what-is-layered-architecture">What is Layered Architecture?</h2>

<p>According to Mark Richards, layered architecture is one of the most common architectural styles, especially in enterprise applications. The idea is to organize the system into groups of well defined responsibilities, which we call layers. Each layer has a specific role, and they communicate with each other in an organized way.</p>

<p>The main goal is to separate responsibilities so that each part of the system takes care of just one thing, without needing to know implementation details from the others.</p>

<p>Notice how similar this is to the way Rails separates its folders and files?</p>

<p>A classic example is the split into three layers: presentation, business, and data.</p>

<ul>
  <li><strong>Presentation</strong> is the layer that interacts with the user. In a web context, this is where views, controllers, and HTTP responses live.</li>
  <li><strong>Business</strong> is where the application logic lives. The rules, the processes, what the system actually does.</li>
  <li><strong>Data</strong> is responsible for accessing and persisting information, whether in a database, a cache, or an external service.</li>
</ul>

<p>In a Rails application, this shows up pretty directly:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Presentation  →  Controllers, Views
Business      →  Models, Service Objects
Data          →  ActiveRecord, repositories
</code></pre></div></div>

<p>When a user makes a request, it hits the controller (presentation), which calls the business logic, which in turn accesses the data. The flow goes down through the layers.</p>

<h2 id="isolation-between-layers">Isolation Between Layers</h2>

<p>Alright, but having layers isn’t enough. What really makes this style work is the isolation between them.</p>

<p>Isolation means each layer only knows what it needs to do its own job.</p>

<p>The presentation layer doesn’t need to know how data is stored in the database. The data layer doesn’t need to understand the business rules.</p>

<p>This is guaranteed by <strong>contracts between layers</strong>. Each layer exposes a clear interface to whatever’s above it. The layer above calls that interface, without depending on the details of how it was implemented underneath.</p>

<p>Think about it this way: the controller calls <code class="language-plaintext highlighter-rouge">OrderService.complete(order)</code>. It doesn’t need to know if that hits the database, sends an email, calls an external API, or anything else. It only knows that contract exists and what it’s going to get back.</p>

<p>This reduces coupling between the parts of the system. If you need to change how an order gets completed, you change the service. The controller doesn’t need to be touched.</p>

<p>Oh, and if you want to know more about coupling, I suggest checking out <a href="/en/what-is-coupling">this post</a> here on the blog.</p>

<p>Let’s look at an example. Imagine a controller that fetches a user’s recent orders:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># without isolation: the controller knows too much</span>
<span class="k">class</span> <span class="nc">OrdersController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">index</span>
    <span class="vi">@orders</span> <span class="o">=</span> <span class="no">Order</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">user_id: </span><span class="n">current_user</span><span class="p">.</span><span class="nf">id</span><span class="p">)</span>
                   <span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="s2">"created_at &gt;= ?"</span><span class="p">,</span> <span class="mi">30</span><span class="p">.</span><span class="nf">days</span><span class="p">.</span><span class="nf">ago</span><span class="p">)</span>
                   <span class="p">.</span><span class="nf">order</span><span class="p">(</span><span class="ss">created_at: :desc</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This controller is navigating straight through the database structure. If the query changes, the controller changes with it.</p>

<p>With isolation:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># with isolation: the controller only knows the contract</span>
<span class="k">class</span> <span class="nc">OrdersController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">index</span>
    <span class="vi">@orders</span> <span class="o">=</span> <span class="no">Order</span><span class="p">.</span><span class="nf">recent_for</span><span class="p">(</span><span class="n">current_user</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">Order</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">scope</span> <span class="ss">:recent_for</span><span class="p">,</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">where</span><span class="p">(</span><span class="ss">user: </span><span class="n">user</span><span class="p">)</span>
      <span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="s2">"created_at &gt;= ?"</span><span class="p">,</span> <span class="mi">30</span><span class="p">.</span><span class="nf">days</span><span class="p">.</span><span class="nf">ago</span><span class="p">)</span>
      <span class="p">.</span><span class="nf">order</span><span class="p">(</span><span class="ss">created_at: :desc</span><span class="p">)</span>
  <span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now the controller only knows there’s a <code class="language-plaintext highlighter-rouge">recent_for</code>. The details stay hidden inside the data layer.</p>

<p>And what happens when that contract gets ignored?</p>

<p>Imagine another part of the application also needs a user’s recent orders. Without isolation, someone’s going to copy the same query into the new controller. When the rule changes, for example the window going from 30 to 60 days, who remembers to update it everywhere? That’s the kind of problem that starts small and keeps growing along with the application.</p>

<p>With <code class="language-plaintext highlighter-rouge">recent_for</code> centralized in the model, the change happens in one place. Whoever calls it doesn’t even need to know something changed.</p>

<p>This isolation also defines how the flow happens. Each request moves through the layers in sequence, top to bottom. Presentation calls business, business calls data. Always in that order.</p>

<p>But then a question comes up: is that flow always mandatory? Does every request need to go through every layer?</p>

<p>The answer depends on how the layers are defined in the system. And that’s where the distinction between closed and open layers comes in.</p>

<h2 id="types-of-layers">Types of Layers</h2>

<h3 id="closed-layers">Closed Layers</h3>

<p>In a closed layers model, a request needs to go through every layer, in order, with no skipping.</p>

<p>In other words, the request comes from presentation, passes through business, and reaches data. It can’t go straight from presentation to the database, skipping the middle.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Presentation
     ↓
  Business
     ↓
   Data
</code></pre></div></div>

<p>This reinforces isolation because it guarantees each layer only interacts with the layer immediately below it. No layer has direct access to things outside its neighborhood.</p>

<p><strong>Advantages:</strong></p>

<p>The main gain is that isolation is guaranteed by the structure, not by developer discipline. This matters especially on larger teams, with devs joining and leaving. Trusting that everyone will respect the contracts on their own is risky. With closed layers, the architecture enforces it automatically.</p>

<p>On top of that, a request’s flow is easy to follow. You know it’s always going to go through the same sequence, so when something breaks, you know where to look.</p>

<p><strong>Disadvantages:</strong></p>

<p>The problem shows up in simple cases.</p>

<p>Imagine a report screen that just needs to count how many orders were placed this month. There’s no business rule involved, it’s just a direct database query. But with closed layers, you still have to create a service that calls the model, that calls the database. Layers just to follow protocol, without adding anything.</p>

<p>In high volume systems, this can also weigh on performance. Every request passes through more points, which means more execution time.</p>

<p>Mark Richards calls this problem the <em>sinkhole anti-pattern</em>: when most requests just pass through the layers without any logic, the cost of the indirection stops being worth it.</p>

<p>And that brings us to the alternative for when this mandatory path starts weighing more than it helps.</p>

<h3 id="open-layers">Open Layers</h3>

<p>In the open layers model, some layers are optional. The request can skip one of them when it makes sense.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Presentation
     ↓
  Business   ← can be skipped
     ↓
   Data
</code></pre></div></div>

<p>Imagine you have an endpoint that just needs to list an order’s available statuses. There’s no business logic involved. In this case, it makes sense for the controller to access the data directly:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">OrdersController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">statuses</span>
    <span class="n">render</span> <span class="ss">json: </span><span class="no">Order</span><span class="p">.</span><span class="nf">statuses</span><span class="p">.</span><span class="nf">keys</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>No service in between, no indirection. Simple, fast, direct.</p>

<p><strong>Advantages:</strong></p>

<p>For cases like this, where the middle layer isn’t doing anything useful, the flexibility to skip it is welcome. The code stays leaner and you avoid creating abstractions that exist just to exist.</p>

<p><strong>Disadvantages:</strong></p>

<p>The risk is the team starting to skip layers frequently, without much criteria. At first it seems reasonable: “this case is simple, it doesn’t need the service.” But that accumulates, and over time it gets hard to tell which exceptions were justified and which turned into shortcuts. A request’s flow stops being predictable, and understanding the whole system gets harder.</p>

<p>In practice, most applications use a mix of both models. Closed layers as the general rule, with specific exceptions where skipping makes sense, as long as the team is disciplined about telling one from the other.</p>

<p>Now that we understand what layers are and how they relate to each other, it’s worth taking a more critical look at this style: what does it solve well, and where does it start showing limitations?</p>

<h2 id="trade-offs-of-layered-architecture">Trade-offs of Layered Architecture</h2>

<p>Like any architectural decision, there’s no absolute right or wrong. It depends on the application’s context.</p>

<p>On the positive side, it’s a style that’s simple to understand and easy to explain to teams. Any dev who joins a project organized in layers can find their way around quickly: where’s the logic? In business. Where’s the query? In data. The flow follows a clear direction and each part has a defined responsibility, which makes it easier to maintain and test.</p>

<p>On the negative side, in simple systems it can create unnecessary indirection. Sometimes you end up creating a service layer that just passes the call through to the model, without doing anything special. Extra code with no real gain.</p>

<p>There’s also the risk of overdoing the abstractions. It can become a habit to create <a href="/en/service-objects-rails">service objects</a>, repositories, and interfaces for everything, even when it’s not needed. Abstractions have a cost. More files, more jumps to understand the code, more things to maintain.</p>

<p>And when the application grows a lot, two problems show up together:</p>

<ul>
  <li>The business layer can turn into a giant catch-all, with service objects from completely different domains thrown into the same place.</li>
  <li>This model leads you to think about “where does this code technically go?” but not necessarily “what part of the business does this belong to?”. In larger systems, organizing by domain usually makes more sense than organizing by technical type.</li>
</ul>

<p>That said, these problems tend to show up once the application has already grown quite a bit. For most day to day cases, layered architecture handles what needs to be handled just fine.</p>

<p>I hope this made it clearer how this style works!</p>

<p>See you next time.</p>]]></content><author><name>Aline Marques</name></author><category term="Software Architecture" /><category term="Ruby on Rails" /><summary type="html"><![CDATA[Understand what layered architecture is, how isolation between layers works, the difference between closed and open layers, and the trade-offs of this architectural style.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://alinemarques.dev/assets/images/logo.png" /><media:content medium="image" url="https://alinemarques.dev/assets/images/logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="en"><title type="html">Coupling: what it is, types, and how to decouple it</title><link href="https://alinemarques.dev/en/what-is-coupling" rel="alternate" type="text/html" title="Coupling: what it is, types, and how to decouple it" /><published>2026-05-31T00:00:00-03:00</published><updated>2026-05-31T00:00:00-03:00</updated><id>https://alinemarques.dev/acoplamento.en</id><content type="html" xml:base="https://alinemarques.dev/what-is-coupling"><![CDATA[<p>When we talk about software architecture, one topic that always comes up is coupling, and whenever we talk about it, we tend to think mostly about classes or objects that depend on each other.</p>

<p>That kind of thinking isn’t wrong, but it only captures part of an idea that goes further than that.</p>

<p>To get started, let’s understand what coupling actually is.</p>

<h2 id="what-is-coupling">What is coupling?</h2>

<p>Coupling, according to Mark Richards, is when components are connected in such a way that changing one will impact the behavior of another component.</p>

<p>So, basically, we’re talking about the dependency between components of a system.</p>

<p>The more coupled the components, the harder it becomes to maintain or test them, because you’ll always depend on another component to do it.</p>

<p>And just to be clear, when we say “component”, it can mean a class, a module, a whole service, a domain, and so on.</p>

<p>Let’s think about an example.</p>

<p>Imagine an e-commerce with two separate services:</p>

<ul>
  <li>Orders</li>
  <li>Inventory</li>
</ul>

<p>Every time a new order comes in, the Orders service calls the Inventory service to check if the product is available, and only then confirms the order to the customer.</p>

<p>These two services don’t share any code. They’re separate codebases, different teams. But they’re highly coupled, because if the Inventory service goes down, the Orders service stops working right along with it.</p>

<p>This is an example of coupling at the service level, not the class level. And it’s exactly the kind of thing we miss when we only think about coupling at the code level.</p>

<p>To understand this “beyond” better, Mark Richards splits coupling into two types: static and dynamic.</p>

<h2 id="types-of-coupling">Types of coupling</h2>

<h3 id="static">Static</h3>

<p>Static coupling is about structural dependencies, meaning things that exist regardless of whether the system is running or not. It’s the kind of coupling you see by looking at the architecture, not the behavior.</p>

<p>A classic example is a shared database. Imagine our e-commerce’s Orders and Reports services both access the same <code class="language-plaintext highlighter-rouge">orders</code> table in the database.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># order service</span>
<span class="k">class</span> <span class="nc">OrderService</span>
  <span class="k">def</span> <span class="nf">order_total</span><span class="p">(</span><span class="n">order_id</span><span class="p">)</span>
    <span class="no">Order</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">id: </span><span class="n">order_id</span><span class="p">).</span><span class="nf">sum</span><span class="p">(</span><span class="ss">:value</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="c1"># revenue report service</span>
<span class="k">class</span> <span class="nc">RevenueReport</span>
  <span class="k">def</span> <span class="nf">monthly_revenue</span>
    <span class="no">Order</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="s2">"created_at &gt;= ?"</span><span class="p">,</span> <span class="mi">30</span><span class="p">.</span><span class="nf">days</span><span class="p">.</span><span class="nf">ago</span><span class="p">).</span><span class="nf">sum</span><span class="p">(</span><span class="ss">:value</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The two services never call each other directly. But if the <code class="language-plaintext highlighter-rouge">value</code> column gets renamed to <code class="language-plaintext highlighter-rouge">total_value</code>, both break. The dependency isn’t in one calling the other’s code, it’s in the shared database structure.</p>

<p>Another place this shows up a lot is inside a Rails monolith, when modules from different domains access each other’s models directly.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># billing module directly accessing the user model</span>
<span class="k">class</span> <span class="nc">BillingService</span>
  <span class="k">def</span> <span class="nf">generate_invoice</span><span class="p">(</span><span class="n">user_id</span><span class="p">)</span>
    <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">user_id</span><span class="p">)</span>
    <span class="n">address</span> <span class="o">=</span> <span class="n">user</span><span class="p">.</span><span class="nf">profile</span><span class="p">.</span><span class="nf">billing_address</span>
    <span class="no">Invoice</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="ss">user: </span><span class="n">user</span><span class="p">,</span> <span class="ss">address: </span><span class="n">address</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">BillingService</code> knows that <code class="language-plaintext highlighter-rouge">User</code> has a <code class="language-plaintext highlighter-rouge">Profile</code>, and that this profile has a <code class="language-plaintext highlighter-rouge">billing_address</code>. If the internal structure of <code class="language-plaintext highlighter-rouge">User</code> or <code class="language-plaintext highlighter-rouge">Profile</code> changes, the billing module feels it, even though it’s a completely different domain.</p>

<h3 id="dynamic">Dynamic</h3>

<p>Dynamic coupling happens when two components need to be available at the same time for the system to work.</p>

<p>This is exactly the example we saw earlier: the Orders service calling the Inventory service synchronously. If Inventory is down, Orders gets stuck right along with it, because it needs an immediate response to continue.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># order service</span>
<span class="k">class</span> <span class="nc">OrdersController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">create</span>
    <span class="c1"># if InventoryService doesn't respond, this line fails</span>
    <span class="n">available</span> <span class="o">=</span> <span class="no">InventoryService</span><span class="p">.</span><span class="nf">check_stock</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:product_id</span><span class="p">])</span>
    <span class="k">return</span> <span class="n">render</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">error: </span><span class="s2">"Out of stock"</span> <span class="p">},</span> <span class="ss">status: </span><span class="mi">422</span> <span class="k">unless</span> <span class="n">available</span>

    <span class="no">Order</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="n">order_params</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>There’s nothing wrong with the code itself. The problem is architectural: a synchronous call creates a time dependency between the two services. If one goes down, the other feels it.</p>

<p>And this kind of coupling doesn’t only show up in HTTP calls. Scheduled jobs suffer from it too, let’s look at another example.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># job that aggregates sales data (runs at midnight)</span>
<span class="k">class</span> <span class="nc">AggregateSalesJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
  <span class="k">def</span> <span class="nf">perform</span>
    <span class="no">Sale</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="s2">"created_at &gt;= ?"</span><span class="p">,</span> <span class="mi">1</span><span class="p">.</span><span class="nf">day</span><span class="p">.</span><span class="nf">ago</span><span class="p">).</span><span class="nf">each</span> <span class="p">{</span> <span class="o">|</span><span class="n">sale</span><span class="o">|</span> <span class="no">SalesReport</span><span class="p">.</span><span class="nf">aggregate</span><span class="p">(</span><span class="n">sale</span><span class="p">)</span> <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="c1"># job that emails the report (runs at 00:30)</span>
<span class="k">class</span> <span class="nc">SendDailyReportJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
  <span class="k">def</span> <span class="nf">perform</span>
    <span class="n">report</span> <span class="o">=</span> <span class="no">SalesReport</span><span class="p">.</span><span class="nf">daily_summary</span>
    <span class="no">ReportMailer</span><span class="p">.</span><span class="nf">daily</span><span class="p">(</span><span class="n">report</span><span class="p">).</span><span class="nf">deliver_now</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>If <code class="language-plaintext highlighter-rouge">AggregateSalesJob</code> takes longer than 30 minutes, <code class="language-plaintext highlighter-rouge">SendDailyReportJob</code> fires with incomplete data. Neither job calls the other, but they’re coupled through timing.</p>

<p>Now that we understand what coupling is and how it shows up, it’s worth talking about how to reduce it.</p>

<p>But it’s worth making clear that the goal isn’t zero coupling. Every system has, and needs, some coupling, otherwise components wouldn’t even talk to each other. What we want is to avoid unnecessary coupling.</p>

<h2 id="how-to-decouple">How to decouple</h2>

<p>The answer depends on the type of coupling, so let’s look at each one separately.</p>

<p>For dynamic coupling, a common approach is to swap the synchronous call for asynchronous communication. Instead of waiting for a response from Inventory to confirm the order, the Orders service publishes an event and moves on.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># before: synchronous, dynamic coupling</span>
<span class="k">def</span> <span class="nf">create</span>
  <span class="n">available</span> <span class="o">=</span> <span class="no">InventoryService</span><span class="p">.</span><span class="nf">check_stock</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:product_id</span><span class="p">])</span>
  <span class="k">return</span> <span class="n">render</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">error: </span><span class="s2">"Out of stock"</span> <span class="p">},</span> <span class="ss">status: </span><span class="mi">422</span> <span class="k">unless</span> <span class="n">available</span>

  <span class="no">Order</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="n">order_params</span><span class="p">)</span>
<span class="k">end</span>

<span class="c1"># after: asynchronous, no dynamic coupling</span>
<span class="k">def</span> <span class="nf">create</span>
  <span class="n">order</span> <span class="o">=</span> <span class="no">Order</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="n">order_params</span><span class="p">.</span><span class="nf">merge</span><span class="p">(</span><span class="ss">status: :pending</span><span class="p">))</span>
  <span class="no">OrderCreatedEvent</span><span class="p">.</span><span class="nf">publish</span><span class="p">(</span><span class="ss">order_id: </span><span class="n">order</span><span class="p">.</span><span class="nf">id</span><span class="p">,</span> <span class="ss">product_id: </span><span class="n">params</span><span class="p">[</span><span class="ss">:product_id</span><span class="p">])</span>

  <span class="n">render</span> <span class="ss">json: </span><span class="n">order</span><span class="p">,</span> <span class="ss">status: :created</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The order is created with a <code class="language-plaintext highlighter-rouge">pending</code> status and an event gets published. The Inventory service listens for that event and processes it when it can. If Inventory is down, Orders doesn’t even know, and doesn’t need to know.</p>

<p>For static coupling, the idea is to keep a component from navigating another component’s internal structure. This is where the <strong>Law of Demeter</strong> comes in.</p>

<h3 id="law-of-demeter">Law of Demeter</h3>

<p>The Law of Demeter says an object should only talk to its direct neighbors, without navigating through the structure of other objects.</p>

<p>Direct neighbors are what the object knows firsthand: itself, the parameters it receives, and the objects it created or already holds as an attribute.</p>

<p>Navigating, in this context, means leaving one object and crossing through others to get where you want.</p>

<p>When you write <code class="language-plaintext highlighter-rouge">order.customer.address.city</code>, you’re navigating: you start at <code class="language-plaintext highlighter-rouge">order</code>, pass through <code class="language-plaintext highlighter-rouge">customer</code>, pass through <code class="language-plaintext highlighter-rouge">address</code>, and only then reach <code class="language-plaintext highlighter-rouge">city</code>. Each point is a different object being crossed.</p>

<p>Let’s look at an example.</p>

<p>Imagine an <code class="language-plaintext highlighter-rouge">InvoiceService</code> that needs the customer’s city to calculate tax:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">InvoiceService</span>
  <span class="k">def</span> <span class="nf">generate</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
    <span class="n">city</span> <span class="o">=</span> <span class="n">order</span><span class="p">.</span><span class="nf">customer</span><span class="p">.</span><span class="nf">address</span><span class="p">.</span><span class="nf">city</span>
    <span class="n">tax_rate</span> <span class="o">=</span> <span class="no">TaxCalculator</span><span class="p">.</span><span class="nf">rate_for</span><span class="p">(</span><span class="n">city</span><span class="p">)</span>
    <span class="c1"># ...</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The problem here is that <code class="language-plaintext highlighter-rouge">InvoiceService</code> knows <code class="language-plaintext highlighter-rouge">Order</code> has a <code class="language-plaintext highlighter-rouge">Customer</code>, which has an <code class="language-plaintext highlighter-rouge">Address</code>, which has a <code class="language-plaintext highlighter-rouge">city</code>. It’s coupled to that whole chain.</p>

<p>If the structure of <code class="language-plaintext highlighter-rouge">Address</code> changes, for example if the city moves into a <code class="language-plaintext highlighter-rouge">Location</code> object, <code class="language-plaintext highlighter-rouge">InvoiceService</code> breaks even though it has nothing to do with that model.</p>

<p>One possible solution is to make <code class="language-plaintext highlighter-rouge">Order</code> expose only what the outside needs to know. In Rails, we do that with <code class="language-plaintext highlighter-rouge">delegate</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Order</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">belongs_to</span> <span class="ss">:customer</span>
  <span class="n">delegate</span> <span class="ss">:city</span><span class="p">,</span> <span class="ss">to: :shipping_address</span><span class="p">,</span> <span class="ss">prefix: :shipping</span>

  <span class="k">def</span> <span class="nf">shipping_address</span>
    <span class="n">customer</span><span class="p">.</span><span class="nf">address</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">InvoiceService</span>
  <span class="k">def</span> <span class="nf">generate</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
    <span class="n">tax_rate</span> <span class="o">=</span> <span class="no">TaxCalculator</span><span class="p">.</span><span class="nf">rate_for</span><span class="p">(</span><span class="n">order</span><span class="p">.</span><span class="nf">shipping_city</span><span class="p">)</span>
    <span class="c1"># ...</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now <code class="language-plaintext highlighter-rouge">InvoiceService</code> only knows about <code class="language-plaintext highlighter-rouge">Order</code>. If <code class="language-plaintext highlighter-rouge">Address</code> changes internally, only <code class="language-plaintext highlighter-rouge">Order</code> needs to be updated. The coupling stays contained in the right place.</p>

<p>Isolating this kind of logic in a dedicated object, like a <a href="/en/service-objects-rails">service object</a>, is another common way to contain this coupling.</p>

<p>It’s not an absolute rule, but it’s a good sign that a component knows too much about another one’s internal structure.</p>

<p>At the core, the idea is this: each object hides its own structure and exposes only what’s needed. Whoever’s outside doesn’t need to know how things are organized internally, right?</p>

<p>So, we’ve now got a bit of an understanding of how coupling works. It’s one of those concepts that seems simple in its definition, but gets more interesting the more you start seeing it at different levels.</p>

<p>And keep in mind I only covered a few examples of coupling, because the truth is it can show up in code in a lot of different ways.</p>

<p>If you want to know how automated tools help protect the application from this kind of problem on an ongoing basis, I wrote a post about <a href="/en/static-analysis">static analysis</a>.</p>

<p>Well, I hope this helped you understand a bit more about this topic.</p>

<p>See you next time!</p>]]></content><author><name>Aline Marques</name></author><category term="Software Architecture" /><category term="Ruby on Rails" /><summary type="html"><![CDATA[Coupling is one of the most important concepts in software architecture. Let's explore what coupling is, why it matters, and how to think about it beyond dependency between classes.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://alinemarques.dev/assets/images/logo.png" /><media:content medium="image" url="https://alinemarques.dev/assets/images/logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="en"><title type="html">Static Analysis: beyond linting</title><link href="https://alinemarques.dev/en/static-analysis" rel="alternate" type="text/html" title="Static Analysis: beyond linting" /><published>2026-05-17T00:00:00-03:00</published><updated>2026-05-17T00:00:00-03:00</updated><id>https://alinemarques.dev/analise-estatica.en</id><content type="html" xml:base="https://alinemarques.dev/static-analysis"><![CDATA[<p>The other day, studying Software Architecture, I ran into fitness functions, a concept that shows up a lot in architecture and refers to automated mechanisms used to continuously verify that the application keeps respecting certain structural rules and architectural characteristics defined by the team.</p>

<p>The idea is that an application’s architecture shouldn’t depend only on devs remembering it, or on documentation that often only gets read during onboarding. The architecture needs to be validated continuously, and fitness functions help with exactly that.</p>

<p>But something important is that a fitness function isn’t a specific tool, it’s an architectural concept that can be applied in a lot of different ways.</p>

<p>Static analysis tools, automated tests, CI/CD pipelines, observability, and monitoring can all be used as ways to implement fitness functions in practice, as long as they’re continuously validating some important rule or architectural characteristic of the application.</p>

<p>So what I want to explore in this post is one of these forms of application: static analysis.</p>

<h2 id="what-is-static-analysis">What is static analysis?</h2>

<p>Static analysis is a technique that involves analyzing code without running the application. That means we’re not going to boot the server, run requests, click on something in the interface, or execute any real system flow.</p>

<p>The analysis happens directly on the source code, and it tries to identify patterns, risks, and problems that can be detected even before the application runs.</p>

<p>Depending on the tool, this can involve complexity analysis, dependencies between modules, security vulnerabilities, project conventions, improper <a href="/en/what-is-coupling">coupling</a>, and other issues related to the system’s structure.</p>

<p>In practice, static analysis can be used to detect things like dead code, unused variables, high complexity, vulnerabilities, forbidden dependencies between parts of the application, convention breaks, and several other types of problems.</p>

<p>Let’s look at an example:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">OrdersController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">index</span>
    <span class="k">if</span> <span class="n">params</span><span class="p">[</span><span class="ss">:status</span><span class="p">].</span><span class="nf">present?</span>
      <span class="vi">@orders</span> <span class="o">=</span> <span class="no">Order</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="s2">"status = '</span><span class="si">#{</span><span class="n">params</span><span class="p">[</span><span class="ss">:status</span><span class="p">]</span><span class="si">}</span><span class="s2">'"</span><span class="p">)</span>
    <span class="k">else</span>
      <span class="vi">@orders</span> <span class="o">=</span> <span class="no">Order</span><span class="p">.</span><span class="nf">all</span>
    <span class="k">end</span>

    <span class="k">if</span> <span class="n">params</span><span class="p">[</span><span class="ss">:user_id</span><span class="p">].</span><span class="nf">present?</span>
      <span class="vi">@orders</span> <span class="o">=</span> <span class="vi">@orders</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">user_id: </span><span class="n">params</span><span class="p">[</span><span class="ss">:user_id</span><span class="p">])</span>
    <span class="k">end</span>

    <span class="vi">@orders</span> <span class="o">=</span> <span class="vi">@orders</span><span class="p">.</span><span class="nf">order</span><span class="p">(</span><span class="s2">"created_at </span><span class="si">#{</span><span class="n">params</span><span class="p">[</span><span class="ss">:direction</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Reading this code, a static analysis tool could already raise a few points of attention.</p>

<p>The first problem is in how the query gets built:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Order</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="s2">"status = '</span><span class="si">#{</span><span class="n">params</span><span class="p">[</span><span class="ss">:status</span><span class="p">]</span><span class="si">}</span><span class="s2">'"</span><span class="p">)</span>
</code></pre></div></div>

<p>Since the value comes straight from <code class="language-plaintext highlighter-rouge">params</code>, there’s a risk of SQL Injection. A tool like Brakeman could flag this snippet as vulnerable, because the application is interpolating external input directly inside a SQL query.</p>

<p>A similar point shows up in the <code class="language-plaintext highlighter-rouge">order</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="vi">@orders</span><span class="p">.</span><span class="nf">order</span><span class="p">(</span><span class="s2">"created_at </span><span class="si">#{</span><span class="n">params</span><span class="p">[</span><span class="ss">:direction</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
</code></pre></div></div>

<p>Here too, user input directly influences the query. Even if the intention is to only allow <code class="language-plaintext highlighter-rouge">asc</code> or <code class="language-plaintext highlighter-rouge">desc</code>, the code doesn’t guarantee that. The safer approach would be to explicitly validate which values are allowed before using that parameter.</p>

<p>Besides the security side, a tool like RuboCop could also complain about the method’s complexity. <code class="language-plaintext highlighter-rouge">index</code> starts out simple, but it already has conditionals, query building, and filtering rules all in the same place. In a real case, this kind of method tends to grow with more filters, more ordering, and more permission rules.</p>

<p>This example shows something important: static analysis doesn’t need to execute the flow to notice risks. It can look at the code’s structure and identify patterns that usually indicate a problem, like external input used in a query, methods growing too much, or mixed responsibilities in the controller.</p>

<h2 id="what-kinds-of-problems-does-static-analysis-try-to-solve">What kinds of problems does static analysis try to solve?</h2>

<p>Something that started catching my attention while studying static analysis is that it doesn’t try to solve just one type of problem.</p>

<p>Before studying this, I had a pretty simplified view that tools like this were basically for “standardizing code” or complaining about style. But in practice, there are different tools trying to protect completely different things inside the system.</p>

<p>There are tools more focused on consistency and project conventions, others focused on security vulnerabilities, and others trying to control complexity or improper coupling between modules of the application.</p>

<p>That’s where things got more interesting: static analysis goes way beyond linting.</p>

<p>In Rails applications, for example, one tool might try to identify SQL Injection risk without running the application, while another might analyze whether certain modules are coupling in a way the team decided to avoid architecturally.</p>

<p>Different tools end up protecting different characteristics of the system.</p>

<h3 id="tools-in-the-rubyrails-ecosystem">Tools in the Ruby/Rails ecosystem</h3>

<p>When I started researching static analysis in the Ruby/Rails ecosystem more, a few tools kept showing up constantly. But what caught my attention was that they don’t try to solve the same kind of problem.</p>

<p>Each of them seems to focus on protecting a different characteristic of the application.</p>

<h4 id="rubocop">RuboCop</h4>

<p>One of the best known tools in the Ruby ecosystem is <a href="https://rubocop.org" target="_blank" rel="noopener noreferrer">RuboCop</a>.</p>

<p>At first, it’s common to associate RuboCop only with code standardization, like single or double quotes, line length, spacing, and writing conventions. And that really is part of its role.</p>

<p>But RuboCop goes beyond that. It can also spot problems related to code complexity and maintainability.</p>

<p>For example, imagine a method like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">process_order</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
  <span class="k">if</span> <span class="n">order</span><span class="p">.</span><span class="nf">paid?</span>
    <span class="k">if</span> <span class="n">order</span><span class="p">.</span><span class="nf">items</span><span class="p">.</span><span class="nf">any?</span>
      <span class="k">if</span> <span class="n">order</span><span class="p">.</span><span class="nf">customer</span><span class="p">.</span><span class="nf">active?</span>
        <span class="n">send_confirmation_email</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
        <span class="n">update_inventory</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
        <span class="n">notify_analytics</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
      <span class="k">else</span>
        <span class="n">cancel_order</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
      <span class="k">end</span>
    <span class="k">else</span>
      <span class="n">mark_as_invalid</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">else</span>
    <span class="n">send_payment_reminder</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Even without running this code, a tool like RuboCop could point out that this method has a lot of complexity, a lot of possible paths, and too many responsibilities concentrated in the same place, a sign that this method is violating the <a href="/en/single-responsibility">single responsibility principle</a>.</p>

<p>The problem here isn’t just “the code looks ugly.” Methods like this tend to be harder to test, harder to change, and easier to break when a new business rule shows up.</p>

<p>In that sense, RuboCop can help protect an important architectural characteristic: <strong>maintainability</strong>.</p>

<p>It doesn’t guarantee the application’s architecture is good, but it can work as a warning sign when parts of the code start growing too much or piling up responsibilities.</p>

<h4 id="brakeman">Brakeman</h4>

<p><a href="https://brakemanscanner.org" target="_blank" rel="noopener noreferrer">Brakeman</a> goes in a different direction. Instead of looking at complexity or conventions, it tries to identify security vulnerabilities in the application.</p>

<p>An example:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">User</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="err">“</span><span class="n">email</span> <span class="o">=</span> <span class="s1">'#{params[:email]}'</span><span class="err">”</span><span class="p">)</span>
</code></pre></div></div>

<p>This snippet, interpolating params straight into the query, is a classic SQL Injection pattern. Brakeman can identify this without needing to run the system, just by reading the code’s structure.</p>

<p>Another type of vulnerability Brakeman also detects is XSS, and a common example in Rails shows up with <code class="language-plaintext highlighter-rouge">html_safe</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">show</span>
  <span class="n">render</span> <span class="ss">html: </span><span class="n">params</span><span class="p">[</span><span class="ss">:message</span><span class="p">].</span><span class="nf">html_safe</span>
<span class="k">end</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">html_safe</code> is a way of telling Rails “you can trust this string, no need to escape it.” By default, Rails escapes any content before rendering it in the browser precisely to avoid this. When you use <code class="language-plaintext highlighter-rouge">html_safe</code> on a params value, anything the user sends gets rendered straight into the page, including scripts. This opens the door to XSS, which is when someone manages to inject and execute code in someone else’s browser.</p>

<p>Brakeman flags this pattern because the combination of <code class="language-plaintext highlighter-rouge">html_safe</code> with external input is a clear sign of risk.</p>

<p>This was what started making it clearer to me that static analysis has a lot more to do with reducing risk than with standardizing code, and that it connects directly to architectural characteristics like <strong>security</strong> and <strong>reliability</strong>.</p>

<h4 id="packwerk">Packwerk</h4>

<p>One of the tools that caught my attention the most while studying this topic was <a href="https://github.com/Shopify/packwerk" target="_blank" rel="noopener noreferrer">Packwerk</a>.</p>

<p>While tools like RuboCop and Brakeman analyze code quality and security more, Packwerk tries to solve a more architectural problem: controlling dependencies between parts of the application.</p>

<p>Its idea is to let you create boundaries inside a Rails monolith, defining which parts of the system can or can’t depend on each other.</p>

<p>This might seem like overkill at first, but in large applications it’s pretty common for the application’s modules to start coupling more and more over time.</p>

<p>For example:</p>

<ul>
  <li>a billing module directly accessing internal analytics code</li>
  <li>a payments context depending on details of the notification system</li>
  <li>an email sending service coupled to business rules from another domain</li>
</ul>

<p>At first this might seem like just a small shortcut. But as the system grows, these dependencies start making the application harder to change, test, and evolve.</p>

<p>What Packwerk does is turn these architectural rules into automated checks.</p>

<p>In other words, instead of the architecture depending only on documentation or code review, the tool itself can warn when certain dependencies considered invalid start showing up inside the system.</p>

<p>There are plenty of other tools besides these. But looking at RuboCop, Brakeman, and Packwerk together, what stands out is that each one is trying to stop a different type of problem from silently growing inside the application, whether that’s complexity, vulnerabilities, or improper coupling.</p>

<p>When I started studying this topic, I thought static analysis was basically a style tool. What I didn’t expect is that it would be one of the most practical ways to make an application’s architecture stop living only in the team’s head and start being verified automatically.</p>

<p>It’s not about picking the right tool. It’s about deciding which characteristics of the application need to be protected, and making sure that protection happens continuously, not just during code review or in documentation.</p>]]></content><author><name>Aline Marques</name></author><category term="Software Architecture" /><category term="Ruby on Rails" /><summary type="html"><![CDATA[Static analysis is often associated with code standardization, but it goes way beyond that. In this post I explore how tools like RuboCop, Brakeman, and Packwerk protect different architectural characteristics of a Rails application.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://alinemarques.dev/assets/images/logo.png" /><media:content medium="image" url="https://alinemarques.dev/assets/images/logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="en"><title type="html">Service Objects: what they are and when to use them</title><link href="https://alinemarques.dev/en/service-objects-rails" rel="alternate" type="text/html" title="Service Objects: what they are and when to use them" /><published>2026-02-05T00:00:00-03:00</published><updated>2026-02-05T00:00:00-03:00</updated><id>https://alinemarques.dev/services-objects.en</id><content type="html" xml:base="https://alinemarques.dev/service-objects-rails"><![CDATA[<p>First of all, we need to understand what service objects are.</p>

<p>If you’ve ever coded in Rails, especially on a large project, you’ve probably already run into them.</p>

<p>Let’s get into it!</p>

<p>Service objects are essentially objects that encapsulate a certain piece of business logic, with the goal of removing complexity from models and controllers.</p>

<p>Usually these objects perform just a single action (process), which keeps you from piling more procedures into other files.</p>

<p>Let me give you an example.</p>

<p>Imagine a simple feature: completing an order in an e-commerce app.</p>

<p>At first glance, it seems simple. The user clicks “checkout” and that’s it.
But behind that button, a bunch of things can happen:</p>

<ul>
  <li>validating that the order is still open</li>
  <li>checking stock</li>
  <li>processing payment</li>
  <li>updating the order status</li>
  <li>sending a confirmation email</li>
  <li>logging events</li>
</ul>

<p>Now comes the important question: <strong>where should this logic live?</strong></p>

<p>In the controller? In the model?</p>

<p>In small applications, you might see all of this in a single place, usually the model.</p>

<p>Does it work? Yes.</p>

<p>The problem is that, over time, this class ends up with too many responsibilities. Small changes to one rule end up affecting behaviors that have nothing directly to do with each other.</p>

<p>On top of that, it makes the code harder to maintain and test each part.</p>

<p>This is where a service object comes in, as an object responsible for representing that specific process: completing an order.</p>

<p>Instead of spreading this logic across the controller and the model, the service object centralizes that flow in a single place, leaving each part of the application with a <a href="/en/single-responsibility">clear responsibility</a>.</p>

<p>The controller just orchestrates the request. The model handles the entity’s rules, and the service object coordinates the business logic involved in the process.</p>

<p>Cool? Now let’s understand how this works in practice.</p>

<h2 id="service-objects-in-rails">Service Objects in Rails</h2>

<p>Well, I’ll go ahead and tell you that in Rails a service object is nothing more than a regular Ruby class, but one created with the goal of executing one very specific process.</p>

<p>There’s no official Rails convention specifically for Service Objects, especially since MVC architecture only separates things into models, views, and controllers.</p>

<p>Service objects end up being a way to expand the business layer once it starts growing beyond what the model can hold. If you want to understand better how this layered organization works, check out this post about <a href="/en/layered-architecture">layered architecture</a>.</p>

<p>But back to the topic, you’ll find that the community has settled on a few practices.</p>

<p>The most common place to put service objects is <code class="language-plaintext highlighter-rouge">app/services</code>. But keep in mind you might see other conventions depending on the company you’re working at.</p>

<p>Also, to avoid naming confusion, the community usually follows a few simple patterns.</p>

<p>Generally, service objects have names that clearly represent the action they perform, usually using a verb in the class name. For example:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">CreateUserAccount</code></li>
  <li><code class="language-plaintext highlighter-rouge">SendWelcomeEmail</code></li>
  <li><code class="language-plaintext highlighter-rouge">CompleteOrder</code></li>
</ul>

<p>The idea here isn’t to create a rigid rule, but to make the code easier to read. When someone sees this name, they already know exactly what that object does, without needing to open the implementation.</p>

<p>NOTE: worth repeating again that this isn’t “set in stone”, meaning you can, and probably will, run into other kinds of structures and conventions.</p>

<h3 id="basic-structure-of-a-service-object">Basic Structure of a Service Object</h3>

<p>Well, considering a service object is meant to carry out just one piece of business logic in isolation, its structure doesn’t need to be very complex.</p>

<p>As I mentioned before, it’s usually just a regular Ruby class.</p>

<p>Beyond that, it usually exposes only one public method, commonly called <code class="language-plaintext highlighter-rouge">call</code>, but sometimes it can also be <code class="language-plaintext highlighter-rouge">perform</code>, just remember it’s really more of a naming convention.</p>

<p>With this, every time we need to call the service, we just need to call the class and the method.</p>

<p>A simple example of a service object structure in Rails could look something like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">CompleteOrder</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>
    <span class="vi">@order</span> <span class="o">=</span> <span class="n">order</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">call</span>
    <span class="k">return</span> <span class="kp">false</span> <span class="k">unless</span> <span class="n">order</span><span class="p">.</span><span class="nf">open?</span>

    <span class="n">process_payment</span>
    <span class="n">update_order_status</span>
    <span class="n">send_confirmation_email</span>

    <span class="kp">true</span>
  <span class="k">end</span>

  <span class="kp">private</span>

  <span class="nb">attr_reader</span> <span class="ss">:order</span>

  <span class="k">def</span> <span class="nf">process_payment</span>
    <span class="c1"># payment logic</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">update_order_status</span>
    <span class="n">order</span><span class="p">.</span><span class="nf">update!</span><span class="p">(</span><span class="ss">status: :completed</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">send_confirmation_email</span>
    <span class="c1"># email sending</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Notice this service object only has one action: completing the order.</p>

<p>It’s going to receive the data it needs in <code class="language-plaintext highlighter-rouge">initialize</code>, and the only exposed method is <code class="language-plaintext highlighter-rouge">call</code>, which is responsible for organizing and calling all the other methods to orchestrate the flow.</p>

<p>And notice it doesn’t replace the model or the controller either.</p>

<p>The service object just coordinates the business logic for that specific process.</p>

<p>So, whenever we need to call this service, the controller would only have this one responsibility, and handling the result:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">OrdersController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">complete</span>
    <span class="c1"># Assuming @order was loaded correctly</span>
    <span class="k">if</span> <span class="no">CompleteOrder</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="vi">@order</span><span class="p">).</span><span class="nf">call</span>
      <span class="n">redirect_to</span> <span class="vi">@order</span><span class="p">,</span> <span class="ss">notice: </span><span class="s2">"Order completed successfully"</span>
    <span class="k">else</span>
      <span class="n">redirect_to</span> <span class="vi">@order</span><span class="p">,</span> <span class="ss">alert: </span><span class="s2">"Unable to complete the order"</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>So, the controller just calls <code class="language-plaintext highlighter-rouge">CompleteOrder.new(@order).call</code>, and that way, all the order completion logic doesn’t need to stay <a href="/en/what-is-coupling">coupled</a> to the controller, it lives in a separate part, making the code easier to maintain.</p>

<h2 id="when-to-use-a-service-object">When to Use a Service Object</h2>

<p>Now that we understand the structure and how it’s usually done in Rails, let’s understand when to use it or not.</p>

<p>In general, service objects tend to make more sense when an operation starts involving more than a simple action on a single model.</p>

<p>Think about it with me, if creating a user in the application just requires saving some data to the database, a simple <code class="language-plaintext highlighter-rouge">User.create</code> already solves the problem.</p>

<p>Now, let’s imagine the flow starts growing, and now you need to:</p>

<ul>
  <li>make sure the email isn’t blocklisted or already used in another context</li>
  <li>associate that user with an organization or account</li>
  <li>enqueue a job to send an email</li>
  <li>notify an external service via API</li>
  <li>log events or metrics for this process</li>
</ul>

<p>Notice how the flow grew? All of this couldn’t stay in the model and controller, it won’t be simple to maintain.</p>

<p>Look at how this controller could end up looking with all of that:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">UsersController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">create</span>
    <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">user_params</span><span class="p">)</span>

    <span class="k">if</span> <span class="no">EmailBlocklist</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="nf">email</span><span class="p">)</span>
      <span class="n">redirect_to</span> <span class="n">new_user_path</span><span class="p">,</span> <span class="ss">alert: </span><span class="s2">"Email blocked"</span>
      <span class="k">return</span>
    <span class="k">end</span>

    <span class="n">user</span><span class="p">.</span><span class="nf">save!</span>
    <span class="n">user</span><span class="p">.</span><span class="nf">create_profile</span>
    <span class="n">user</span><span class="p">.</span><span class="nf">organizations</span> <span class="o">&lt;&lt;</span> <span class="n">current_organization</span>

    <span class="no">WelcomeEmailJob</span><span class="p">.</span><span class="nf">perform_later</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="nf">id</span><span class="p">)</span>
    <span class="no">ExternalApiNotifier</span><span class="p">.</span><span class="nf">notify_user_created</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>

    <span class="n">redirect_to</span> <span class="n">user_path</span><span class="p">(</span><span class="n">user</span><span class="p">),</span> <span class="ss">notice: </span><span class="s2">"User created successfully"</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>You can tell right away that this controller isn’t just orchestrating the user creation request, it’s also concentrating all the business rules and integrations.</p>

<p>And here’s an important point… would this code work? Yes! But it would be hard to test and maintain.</p>

<p>Some signs that it’s time to use a service object: the controller starts growing with logic that isn’t its responsibility, the same rule starts showing up in different places (controllers, jobs, callbacks), or the model starts concentrating behaviors that have nothing to do with the entity itself.</p>

<p>Something that helps me a lot is trying to explain a given model or controller out loud to myself. Explaining code is always useful for understanding better what it’s doing, and if there’s too much stuff inside a controller or model that goes beyond their responsibility, that’s already a sign.</p>

<h2 id="when-not-to-use-service-objects">When NOT to Use Service Objects</h2>

<p>Even though they’re very useful, service objects don’t need to be used in every scenario.</p>

<p>Simple CRUD operations, validations that belong to the model, filters that a scope already solves… in these cases, creating a service object just adds unnecessary complexity. The extraction only makes sense when it makes the code easier to understand, not when it’s done out of habit or to “look more organized.”</p>

<p>A concrete example of when not to use one is when the logic clearly belongs to the model itself.</p>

<p>Imagine a simple user model with a validation and a small behavior:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">User</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">validates</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">uniqueness: </span><span class="kp">true</span>

  <span class="k">def</span> <span class="nf">active?</span>
    <span class="n">status</span> <span class="o">==</span> <span class="s2">"active"</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>See, in this case, it doesn’t make sense to pull this logic out into a service object, because the validation and the <code class="language-plaintext highlighter-rouge">active?</code> method are directly about the state of the data, which in this case is a behavior of the <code class="language-plaintext highlighter-rouge">User</code> entity.</p>

<p>See the difference?</p>

<p>Let’s look at an example in the controller:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">UsersController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">create</span>
    <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">user_params</span><span class="p">)</span>

    <span class="k">if</span> <span class="n">user</span><span class="p">.</span><span class="nf">save</span>
      <span class="n">redirect_to</span> <span class="n">user_path</span><span class="p">(</span><span class="n">user</span><span class="p">),</span> <span class="ss">notice: </span><span class="s2">"User created successfully"</span>
    <span class="k">else</span>
      <span class="n">render</span> <span class="ss">:new</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="kp">private</span>

  <span class="k">def</span> <span class="nf">user_params</span>
    <span class="n">params</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:user</span><span class="p">).</span><span class="nf">permit</span><span class="p">(</span><span class="ss">:email</span><span class="p">,</span> <span class="ss">:name</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>In this case, the controller is doing exactly what’s expected of it: receiving the request, creating the record in the database using the model, and handling the response.</p>

<p>So, keep in mind that service objects aren’t a mandatory rule or pattern. They exist to help organize business logic once complexity starts growing.</p>

<p>At the end of the day, the question isn’t “can I use a service object here?”, but rather “does this actually make the code clearer?”.</p>

<p>It’s not something to use to make the code look pretty or to replace models or controllers, they exist to help when the logic starts growing and it gets hard to understand where each responsibility should live.</p>

<p>I hope this helped you understand this pattern better, because you’ll definitely run into it in large applications.</p>

<p>See you next time!</p>]]></content><author><name>Aline Marques</name></author><category term="Ruby on Rails" /><summary type="html"><![CDATA[Understand what Service Objects are in Rails, when to use them, and how to organize business logic in larger applications.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://alinemarques.dev/assets/images/logo.png" /><media:content medium="image" url="https://alinemarques.dev/assets/images/logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>