<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mark Harbison</title>
    <description>The latest articles on DEV Community by Mark Harbison (@markharbison).</description>
    <link>https://dev.to/markharbison</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1248830%2F29a3e039-c11b-4639-977e-12ff1c33d440.png</url>
      <title>DEV Community: Mark Harbison</title>
      <link>https://dev.to/markharbison</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/markharbison"/>
    <language>en</language>
    <item>
      <title>Composability Over Coupling: Evolving Authorization in Rails</title>
      <dc:creator>Mark Harbison</dc:creator>
      <pubDate>Wed, 04 Mar 2026 10:40:52 +0000</pubDate>
      <link>https://dev.to/markharbison/composability-over-coupling-evolving-authorization-in-rails-26c2</link>
      <guid>https://dev.to/markharbison/composability-over-coupling-evolving-authorization-in-rails-26c2</guid>
      <description>&lt;p&gt;Authorization feels simple in the early stages of a Rails application.&lt;/p&gt;

&lt;p&gt;You define a few roles.&lt;br&gt;
You write some policy methods.&lt;br&gt;
You add a handful of conditional checks.&lt;/p&gt;

&lt;p&gt;It works.&lt;/p&gt;

&lt;p&gt;In year one, that's usually enough.&lt;/p&gt;

&lt;p&gt;The tension appears later.&lt;/p&gt;

&lt;p&gt;Features expand. Roles multiply. Exceptions accumulate. A simple "admin vs user" distinction becomes a matrix of capabilities. What started as a few clean policy methods begins to encode assumptions about how permissions are stored and assigned.&lt;/p&gt;

&lt;p&gt;And that's where the real problem surfaces.&lt;/p&gt;

&lt;p&gt;Not complexity.&lt;/p&gt;

&lt;p&gt;Coupling.&lt;/p&gt;

&lt;p&gt;When your permission model changes, how much of your policy layer needs to change with it?&lt;/p&gt;

&lt;p&gt;If the answer is "a lot", then the system isn't just complex. It's entangled.&lt;/p&gt;

&lt;p&gt;In long-lived Rails applications, authorization strategies evolve. Direct permissions become group-based. Groups gain hierarchy. Multi-tenant boundaries appear. Auditing requirements emerge. The persistence model shifts to accommodate new realities.&lt;/p&gt;

&lt;p&gt;Your policies shouldn't need to be rewritten every time that happens.&lt;/p&gt;

&lt;p&gt;Authorization logic will grow. That's normal.&lt;/p&gt;

&lt;p&gt;What matters is whether it grows inside clear boundaries - or across them.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Hidden Coupling in Most Rails Authorization
&lt;/h2&gt;

&lt;p&gt;In many Rails applications, authorization logic begins its life inside policy methods.&lt;/p&gt;

&lt;p&gt;A policy might check a role directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def update?
 user.admin? || user.manager?
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or it may reach into a permissions table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def destroy?
 user.permissions.exists?(name: "employees.destroy")
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's nothing inherently wrong with either approach. They're straightforward. They're readable. They solve the immediate problem.&lt;/p&gt;

&lt;p&gt;The issue is structural.&lt;/p&gt;

&lt;p&gt;In these examples, the policy method is doing more than expressing intent. It's also deciding how that intent is evaluated. It knows where permissions live. It knows how they're queried. It often knows how roles are represented.&lt;/p&gt;

&lt;p&gt;Orchestration and decision logic become intertwined.&lt;/p&gt;

&lt;p&gt;Over time, this pattern scales in ways that aren't immediately obvious. A few direct checks become dozens. Slight variations appear between policies. Edge cases introduce conditionals that reference specific tables or associations. The policy layer starts to mirror the persistence layer.&lt;/p&gt;

&lt;p&gt;At that point, a change to the permission model isn't isolated. It ripples outward.&lt;/p&gt;

&lt;p&gt;If you move from direct permissions to group-based RBAC, policy methods change.&lt;br&gt;
If you introduce tenant scoping, policy methods change.&lt;br&gt;
If you refactor roles into something more granular, policy methods change.&lt;/p&gt;

&lt;p&gt;The policies were meant to answer a simple question: "Is this action allowed?"&lt;/p&gt;

&lt;p&gt;Instead, they've become tightly coupled to how that answer is computed.&lt;/p&gt;

&lt;p&gt;This isn't a critique of a specific authorization strategy. The problem is structure, repeatability, and maintainability.&lt;/p&gt;

&lt;p&gt;The issue isn't RBAC. It's entanglement.&lt;/p&gt;


&lt;h2&gt;
  
  
  Principle 1: Authorization Strategies Should Be Replaceable
&lt;/h2&gt;

&lt;p&gt;To untangle authorization, we need to separate three concerns that are often collapsed into one.&lt;/p&gt;

&lt;p&gt;First, there is the &lt;strong&gt;policy engine&lt;/strong&gt;. This is the orchestration layer. It decides when authorization is evaluated and which policy method is responsible for a given controller action.&lt;/p&gt;

&lt;p&gt;Second, there is the &lt;strong&gt;authorization strategy&lt;/strong&gt;. This is the decision logic. It answers the question: given a user and a context, is this allowed?&lt;/p&gt;

&lt;p&gt;Third, there is the &lt;strong&gt;persistence model&lt;/strong&gt;. This is a domain concern. It defines how roles, groups, permissions, and relationships are stored and associated.&lt;/p&gt;

&lt;p&gt;These are not the same responsibility.&lt;/p&gt;

&lt;p&gt;In many systems, they drift together over time. The policy method calls into a role check that assumes a specific table structure. The role check assumes a particular association model. The persistence layer becomes implicitly coupled to the policy surface.&lt;/p&gt;

&lt;p&gt;That coupling rarely hurts in year one.&lt;/p&gt;

&lt;p&gt;It becomes visible in year three.&lt;/p&gt;

&lt;p&gt;Direct permissions become group-based.&lt;br&gt;
Flat groups acquire hierarchy.&lt;br&gt;
Single-tenant assumptions give way to tenant scoping.&lt;br&gt;
Static roles evolve into dynamic, feature-level permissions.&lt;/p&gt;

&lt;p&gt;None of these shifts are unusual. They're a sign that the system is growing.&lt;/p&gt;

&lt;p&gt;But if your policies are written against a specific storage model, every structural change forces a policy rewrite. The orchestration layer becomes brittle because it knows too much.&lt;/p&gt;

&lt;p&gt;That's unnecessary.&lt;/p&gt;

&lt;p&gt;Policies should express intent.&lt;br&gt;
Strategies should evaluate that intent.&lt;br&gt;
The persistence model should serve the domain.&lt;/p&gt;

&lt;p&gt;If those boundaries are respected, the authorization strategy can evolve without destabilising the policy surface.&lt;/p&gt;

&lt;p&gt;Policies should not care how permissions are stored.&lt;/p&gt;

&lt;p&gt;They should care only that a decision can be made.&lt;/p&gt;


&lt;h2&gt;
  
  
  Principle 2: Policies Should Orchestrate. Rules Should Decide.
&lt;/h2&gt;

&lt;p&gt;If authorization strategies are meant to be replaceable, the structure of the policy layer has to support that.&lt;/p&gt;

&lt;p&gt;This leads to a second principle: &lt;em&gt;Policies should orchestrate. Rules should decide.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A policy sits at the controller boundary. Its responsibility is to map an action to an authorization outcome. It expresses intent.&lt;/p&gt;

&lt;p&gt;When a controller calls &lt;code&gt;update&lt;/code&gt;, the policy answers a single question: &lt;em&gt;is this action allowed?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It shouldn't need to know how that answer is computed.&lt;/p&gt;

&lt;p&gt;Instead, the policy should compose one or more rule objects - small, focused units of decision logic.&lt;/p&gt;

&lt;p&gt;A rule encapsulates the strategy.&lt;br&gt;
It receives the user, the context, and any options.&lt;br&gt;
It returns a boolean.&lt;/p&gt;

&lt;p&gt;Conceptually, a policy method becomes something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;authorized?([PermissionPolicyRule], permission: "employees.read")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The policy declares the required capability.&lt;/p&gt;

&lt;p&gt;The rule determines whether the user satisfies it.&lt;/p&gt;

&lt;p&gt;That separation is subtle, but it changes the system's behaviour over time.&lt;/p&gt;

&lt;p&gt;The policy remains stable because it expresses intent at the level of the application. "This action requires &lt;code&gt;employees.read&lt;/code&gt;." That statement doesn't change just because your group model changes.&lt;/p&gt;

&lt;p&gt;The rule encapsulates the strategy. Today it might query direct permissions. Tomorrow it might traverse hierarchical groups. In a multi-tenant system, it might apply scoping logic. The policy does not need to know.&lt;/p&gt;

&lt;p&gt;The persistence layer becomes an implementation detail of the rule.&lt;br&gt;
This is what composability looks like in practice. Policies compose rules. Rules encapsulate strategies. Strategies rely on the domain model.&lt;/p&gt;

&lt;p&gt;Each layer has a boundary.&lt;/p&gt;

&lt;p&gt;When those boundaries are respected, authorization logic can grow without spreading across the system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Demonstrating the Separation
&lt;/h2&gt;

&lt;p&gt;This separation wasn't an abstract exercise.&lt;/p&gt;

&lt;p&gt;It's the architectural pattern I wanted when building long-lived Rails systems.&lt;/p&gt;

&lt;p&gt;I wanted a policy layer that stayed stable as the application evolved. I wanted authorization strategies that could change without forcing a rewrite of every policy method. And I didn't want the core engine to dictate how permissions were stored.&lt;/p&gt;

&lt;p&gt;That's the motivation behind &lt;a href="https://github.com/CodeTectonics/access_forge" rel="noopener noreferrer"&gt;AccessForge&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;AccessForge handles orchestration. It resolves policies from controllers. It maps actions to predicate methods. It provides a small surface for composing rule objects.&lt;/p&gt;

&lt;p&gt;It does not dictate how your application makes authorization decisions. It ships with two default rules — &lt;strong&gt;always open&lt;/strong&gt; and &lt;strong&gt;always closed&lt;/strong&gt;. The rest is left to you. You provide rule classes based on your own domain model.&lt;/p&gt;

&lt;p&gt;The strategy layer can then be implemented independently.&lt;/p&gt;

&lt;p&gt;This is where &lt;a href="https://github.com/CodeTectonics/access_forge-permissions" rel="noopener noreferrer"&gt;AccessForge::Permissions&lt;/a&gt; fits. It's the first official extension - a permission-based rule built on top of the core engine.&lt;/p&gt;

&lt;p&gt;The contract is deliberately minimal: If &lt;code&gt;user.permissions&lt;/code&gt; exists, a decision can be evaluated.&lt;/p&gt;

&lt;p&gt;That’s it.&lt;/p&gt;

&lt;p&gt;Permissions may be assigned directly to users.&lt;br&gt;
They may be granted through groups.&lt;br&gt;
Groups may be hierarchical.&lt;br&gt;
Permissions may carry metadata or tenant scoping.&lt;/p&gt;

&lt;p&gt;The rule does not enforce a structure. It relies on a boundary.&lt;/p&gt;

&lt;p&gt;If the persistence model evolves, the rule can evolve with it.&lt;br&gt;
If a different authorization strategy is required, a different rule can replace it.&lt;br&gt;
The policies remain unchanged.&lt;/p&gt;

&lt;p&gt;This isn't abstraction for its own sake.&lt;/p&gt;

&lt;p&gt;It's about keeping orchestration stable while allowing strategy and persistence to adapt to the domain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Composability Beats Coupling
&lt;/h2&gt;

&lt;p&gt;The distinction here isn't about elegance. It's about system properties.&lt;/p&gt;

&lt;p&gt;Composability is not a stylistic preference. It's an architectural characteristic.&lt;/p&gt;

&lt;p&gt;When rules are small and focused, they become composable. A policy can rely on a single rule. It can combine multiple rules. It can layer contextual checks without embedding storage assumptions.&lt;/p&gt;

&lt;p&gt;Policies compose rules.&lt;br&gt;
Rules encapsulate strategies.&lt;br&gt;
Strategies depend on the domain model.&lt;/p&gt;

&lt;p&gt;Each layer has a clear responsibility.&lt;/p&gt;

&lt;p&gt;Because of that separation, strategies can evolve independently. A permission rule can change its internal query logic. It can introduce tenant scoping. It can traverse hierarchical group structures. It can even be replaced entirely.&lt;/p&gt;

&lt;p&gt;The policy surface does not need to change.&lt;/p&gt;

&lt;p&gt;Contrast that with coupling.&lt;/p&gt;

&lt;p&gt;If a policy method directly references &lt;code&gt;user.admin?&lt;/code&gt;, it is bound to a specific role model. If permission queries are embedded inside multiple policy methods, logic becomes duplicated. When the persistence structure changes, those assumptions must be hunted down and rewritten.&lt;/p&gt;

&lt;p&gt;The system resists change because its boundaries are unclear.&lt;/p&gt;

&lt;p&gt;This is where many Rails applications begin to accumulate accidental complexity. Not because the framework is limited, but because structural decisions were deferred.&lt;/p&gt;

&lt;p&gt;Flexibility does not come from fewer boundaries.&lt;/p&gt;

&lt;p&gt;It comes from well-defined ones.&lt;/p&gt;

&lt;p&gt;When policies orchestrate and rules decide, the system gains a stable surface and a flexible interior. Authorization can grow in sophistication without spreading across the application.&lt;/p&gt;

&lt;p&gt;Composability over coupling is not an aesthetic choice.&lt;/p&gt;

&lt;p&gt;It's a way to keep long-lived systems evolvable.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Means in Year Three
&lt;/h2&gt;

&lt;p&gt;In the early stages of a product, authorization feels contained.&lt;/p&gt;

&lt;p&gt;By year three, it rarely is.&lt;/p&gt;

&lt;p&gt;Features expand. Capabilities become more granular. What was once a single "manage employees" permission becomes read, write, export, approve, and archive. Different customer tiers require different access levels. Enterprise clients ask for custom role configurations.&lt;/p&gt;

&lt;p&gt;In a SaaS environment, tenant boundaries become stricter. Data isolation requirements increase. Audit trails become mandatory. Internal teams grow, and multiple developers begin touching the authorization layer.&lt;/p&gt;

&lt;p&gt;At that point, authorization is no longer a small concern. It's part of the system's core integrity.&lt;/p&gt;

&lt;p&gt;If policies are tightly coupled to a specific role model or storage structure, each new requirement increases risk. Changes feel invasive. Refactors feel dangerous. Work slows down because the boundaries are unclear.&lt;/p&gt;

&lt;p&gt;But if orchestration, strategy, and persistence are properly separated, growth becomes more predictable.&lt;/p&gt;

&lt;p&gt;New permission structures can be introduced without destabilising policy definitions. Tenant scoping can be added inside rules without rewriting controllers. Audit logic can evolve alongside the strategy layer.&lt;/p&gt;

&lt;p&gt;The surface remains stable while the interior adapts.&lt;/p&gt;

&lt;p&gt;That stability is not about purity. It's about risk reduction.&lt;/p&gt;

&lt;p&gt;Long-lived systems accumulate complexity. That's inevitable.&lt;/p&gt;

&lt;p&gt;The goal is to ensure that complexity accumulates behind boundaries, not across them.&lt;/p&gt;

&lt;p&gt;Authorization doesn't need to be clever.&lt;br&gt;
It needs to remain evolvable.&lt;/p&gt;

&lt;p&gt;That's the design goal behind AccessForge and its first extension.&lt;br&gt;
Not cleverness.&lt;br&gt;
Replaceability.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>architecture</category>
      <category>authorization</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Where Should Analytics Logic Live in a Long-Lived Rails App?</title>
      <dc:creator>Mark Harbison</dc:creator>
      <pubDate>Sat, 07 Feb 2026 17:34:07 +0000</pubDate>
      <link>https://dev.to/markharbison/where-should-analytics-logic-live-in-a-long-lived-rails-app-13ke</link>
      <guid>https://dev.to/markharbison/where-should-analytics-logic-live-in-a-long-lived-rails-app-13ke</guid>
      <description>&lt;h2&gt;
  
  
  The problem as it appears over time
&lt;/h2&gt;

&lt;p&gt;Analytics is rarely a problem in the early life of a Rails application.&lt;/p&gt;

&lt;p&gt;At first, it’s a query in a controller. Then a bit of aggregation in a service. Maybe a chart that embeds some SQL because it was faster at the time. Nothing feels wrong - in fact, it often feels flexible. You can answer new questions quickly, and the code is close to where it’s used.&lt;/p&gt;

&lt;p&gt;As the application grows, teams often try to improve things by introducing structure. A PORO per chart appears. Each report gets its own class. Logic moves out of controllers and into “analytics objects” that feel more intentional and easier to test.&lt;/p&gt;

&lt;p&gt;This helps - briefly.&lt;/p&gt;

&lt;p&gt;Over time, these objects begin to mirror the same underlying problems. Queries are duplicated with small variations. Filtering and aggregation logic diverge subtly between charts. Business rules become scattered across multiple classes with no shared vocabulary. The system looks organised, but the relationships between analytics concepts remain implicit.&lt;/p&gt;

&lt;p&gt;In long-lived systems, this becomes risky.&lt;/p&gt;

&lt;p&gt;Analytics code turns into something teams are hesitant to touch - not because it’s inherently complex, but because its structure makes the impact of change hard to reason about. Onboarding slows down. Small reporting changes feel larger than they should. What once felt flexible now feels fragile.&lt;/p&gt;

&lt;p&gt;The issue isn’t that analytics exists. It’s that, in many Rails applications, it never gets a clear architectural home.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Becomes Dangerous in Long-Lived Systems
&lt;/h2&gt;

&lt;p&gt;In a long-lived Rails application, analytics rarely breaks loudly. Instead, it degrades quietly.&lt;/p&gt;

&lt;p&gt;As analytics logic spreads across controllers, services, and chart-specific POROs, the system accumulates assumptions that aren’t written down anywhere. Filters behave slightly differently depending on where they’re applied. Aggregations are re-implemented with small inconsistencies. Over time, analytics becomes less a coherent part of the system and more a collection of loosely related behaviours.&lt;/p&gt;

&lt;p&gt;The real cost shows up when something needs to change.&lt;/p&gt;

&lt;p&gt;A new business question sounds simple, but requires touching multiple classes. A small tweak to a report risks breaking another chart that happens to reuse the same query in a different form. Developers become cautious - not because the change is difficult, but because the blast radius is unclear.&lt;/p&gt;

&lt;p&gt;This uncertainty increases cognitive load. New team members have to learn not just how analytics works, but where it might be hiding. Context lives in people’s heads rather than in the structure of the system. Analytics slowly turns into “that part of the codebase” that only a few people feel comfortable modifying.&lt;/p&gt;

&lt;p&gt;Ironically, many of these systems were built to be flexible.&lt;/p&gt;

&lt;p&gt;But flexibility without clear boundaries tends to age poorly. What once allowed rapid iteration now creates hesitation, duplication, and accidental coupling. The system hasn’t become rigid - it has become fragile.&lt;/p&gt;

&lt;p&gt;In practice, this often leads teams to avoid improving analytics altogether, or to work around existing structures instead of fixing them. At that point, the problem is no longer about charts or reports. It’s about architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Question
&lt;/h2&gt;

&lt;p&gt;At this point, it’s tempting to reach for tools.&lt;/p&gt;

&lt;p&gt;A new reporting library. A dashboard builder. A BI layer. Something that promises to “solve analytics” by adding capabilities on top of what already exists.&lt;/p&gt;

&lt;p&gt;But the problems described above aren’t really about missing features.&lt;/p&gt;

&lt;p&gt;They’re about ownership, boundaries, and structure.&lt;/p&gt;

&lt;p&gt;The question isn’t how to build charts faster, or how to write more expressive queries. It’s a more fundamental design question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where should analytics logic live in a Rails application that’s expected to evolve over time?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not as an implementation detail, and not as a collection of convenience methods - but as a part of the system with clear responsibilities and a clear place to live.&lt;/p&gt;

&lt;p&gt;This isn’t a question about frontends or visualisation libraries. It’s not even a question about SQL versus ActiveRecord. Those decisions matter, but they sit downstream of a more basic concern: how analytics fits into the overall shape of the application.&lt;/p&gt;

&lt;p&gt;Until that question is answered explicitly, analytics tends to remain incidental. It exists everywhere and nowhere at the same time. And in long-lived systems, that ambiguity becomes expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Different Way to Think About Analytics
&lt;/h2&gt;

&lt;p&gt;If analytics is going to survive in a long-lived Rails application, it needs to stop being treated as a side effect of querying data.&lt;/p&gt;

&lt;p&gt;It needs a shape.&lt;/p&gt;

&lt;p&gt;One useful way to think about analytics is to separate it into a small number of distinct responsibilities - each with a clear purpose and a clear place to live.&lt;/p&gt;

&lt;p&gt;At a high level, analytics work usually answers four different questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;What data exists and is available for analysis?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Where does that data come from?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How is it queried, filtered, and aggregated?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How is it presented or consumed?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In many Rails applications, these concerns are collapsed into one place - often a controller action, a service object, or a PORO created “just for this chart.” That collapse is what makes analytics hard to reason about and harder to change.&lt;/p&gt;

&lt;p&gt;Instead, imagine treating each of these concerns explicitly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separate the What from the How
&lt;/h3&gt;

&lt;p&gt;The first shift is conceptual.&lt;/p&gt;

&lt;p&gt;There is a difference between describing a dataset and querying it.&lt;/p&gt;

&lt;p&gt;A dataset answers questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;What does this data represent?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What is it called in the domain?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Is it stable enough for charts, reports, or dashboards to depend on?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This description changes far less frequently than the queries built on top of it.&lt;/p&gt;

&lt;p&gt;By naming and modelling datasets explicitly, you give analytics a stable anchor - something other parts of the system can reference without knowing how the data is fetched or calculated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make Data Sources Explicit
&lt;/h3&gt;

&lt;p&gt;Next, separate where the data comes from from everything else.&lt;/p&gt;

&lt;p&gt;Sometimes data comes from ActiveRecord models.&lt;br&gt;
Sometimes from database views.&lt;br&gt;
Sometimes from external APIs.&lt;br&gt;
Sometimes from computed or in-memory sources.&lt;/p&gt;

&lt;p&gt;In many codebases, this distinction is implicit and scattered. The source is “whatever the query happens to hit.”&lt;/p&gt;

&lt;p&gt;Making data sources explicit - even when they’re just thin wrappers around existing models - creates a boundary. It gives you a place to say: this is the authoritative source for this analytical data.&lt;/p&gt;

&lt;p&gt;That boundary is what allows analytics logic to evolve without constantly rewriting consumers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Treat Querying as a Strategy, Not an Accident
&lt;/h3&gt;

&lt;p&gt;Filtering, grouping, and aggregation are not incidental details. They’re behaviour.&lt;/p&gt;

&lt;p&gt;When query logic is embedded directly inside controllers, services, or chart POROs, it becomes tightly coupled to its immediate use case. Reuse is accidental. Testing is awkward. Changes ripple unpredictably.&lt;/p&gt;

&lt;p&gt;When querying logic has no explicit home, it still exists - it is just harder to see and harder to change.&lt;/p&gt;

&lt;p&gt;Treating querying as a strategy - something that can vary independently - makes it possible to support different data sources, different querying mechanisms, and different performance trade-offs without reshaping the entire system.&lt;/p&gt;

&lt;p&gt;This doesn’t mean abstracting everything behind clever interfaces. It means giving querying logic a clear role and a clear boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Let Presentation Be the Last Step
&lt;/h3&gt;

&lt;p&gt;Finally, presentation should consume analytics - not define it.&lt;/p&gt;

&lt;p&gt;Charts, reports, and dashboards are views over analytics data. They should describe what they want (grouping, measures, filters, labels), not how the data is fetched or calculated.&lt;/p&gt;

&lt;p&gt;When presentation concerns are kept at the edge, analytics becomes easier to test, easier to reuse, and easier to expose through different frontends - whether that’s a charting library, an admin UI, or an API.&lt;/p&gt;

&lt;p&gt;Taken together, this model does something important:&lt;/p&gt;

&lt;p&gt;It turns analytics from scattered behaviour into a small, explicit subsystem with its own vocabulary and boundaries.&lt;/p&gt;

&lt;p&gt;Not heavier.&lt;br&gt;
Not more abstract.&lt;br&gt;
Just clearer.&lt;/p&gt;

&lt;p&gt;This is usually where teams reach for structure - but not yet for architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The “One PORO per Chart” Pattern - and Why It Breaks Down
&lt;/h2&gt;

&lt;p&gt;A very common response to messy analytics code is to “clean it up” by introducing Plain Old Ruby Objects.&lt;/p&gt;

&lt;p&gt;You end up with things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;SalesByRegionChart&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ActiveUsersOverTime&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;RevenuePerCustomerReport&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each class encapsulates the query, the aggregation, and the formatting needed for a single chart or report. Compared to SQL in controllers, this feels like a big improvement.&lt;/p&gt;

&lt;p&gt;And to be fair - it often is an improvement.&lt;/p&gt;

&lt;p&gt;This pattern usually appears when a team cares about code quality but doesn’t yet have a shared mental model for analytics as a system. POROs are familiar, flexible, and easy to introduce incrementally.&lt;/p&gt;

&lt;p&gt;But over time, cracks start to appear.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why It Feels Right at First
&lt;/h3&gt;

&lt;p&gt;The one-PORO-per-chart approach works initially because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Each chart has a clear owner in the codebase&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Logic is no longer embedded in controllers&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Queries are testable in isolation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Naming things feels like progress&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a while, analytics feels under control.&lt;/p&gt;

&lt;p&gt;The problem isn’t that this pattern is wrong. The problem is that it doesn’t scale with the questions the business starts asking.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the Pattern Starts to Strain
&lt;/h3&gt;

&lt;p&gt;As analytics grows, you start to see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Slight variations of the same query copied across multiple POROs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The same filters implemented in subtly different ways&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Aggregation logic drifting over time&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Formatting concerns leaking into query objects&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No single place to answer: “What analytics datasets does this system actually have?”&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point, change becomes risky.&lt;/p&gt;

&lt;p&gt;A small tweak to a definition - say, what “active user” means - requires hunting through multiple classes, each tightly coupled to its own presentation needs.&lt;/p&gt;

&lt;p&gt;The code is organised, but the system is not.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Hidden Cost: No Stable Analytics Vocabulary
&lt;/h3&gt;

&lt;p&gt;Perhaps the biggest limitation of this pattern is that it never establishes a stable vocabulary for analytics.&lt;/p&gt;

&lt;p&gt;Each chart PORO defines its own implicit dataset.&lt;br&gt;
Each report re-describes what data it cares about.&lt;br&gt;
Nothing is persisted, named, or shared at the domain level.&lt;/p&gt;

&lt;p&gt;That makes it difficult to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Reuse analytics across charts and reports&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Expose analytics through admin tools or APIs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Version or migrate analytics definitions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reason about analytics independently of the UI&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code answers how to build this chart, but not what analytics capabilities the system provides.&lt;/p&gt;

&lt;h3&gt;
  
  
  When POROs Stop Being Enough
&lt;/h3&gt;

&lt;p&gt;Eventually, teams hit a point where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Analytics needs to be configurable, not hard-coded&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Non-developers need visibility into what exists&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Frontends want structured data, not bespoke hashes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Analytics logic must survive refactors and rewrites&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, adding another PORO doesn’t really solve the problem. It just adds another place where knowledge lives.&lt;/p&gt;

&lt;p&gt;What’s missing isn’t another class - it’s a clearer separation of responsibilities and a more explicit model for analytics itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treating Analytics as a First-Class Domain
&lt;/h2&gt;

&lt;p&gt;Long-lived Rails applications tend to stabilise around a few core domains.&lt;/p&gt;

&lt;p&gt;We invest time modelling things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;users&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;accounts&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;permissions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;billing&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;workflows&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We give them names, persistence, constraints, and clear ownership.&lt;/p&gt;

&lt;p&gt;Analytics, by contrast, is often treated as something derived - important, but secondary. It lives in queries, helpers, or one-off objects because it’s “just reporting.”&lt;/p&gt;

&lt;p&gt;But analytics isn’t incidental.&lt;/p&gt;

&lt;p&gt;Analytics encodes how the business understands itself.&lt;/p&gt;

&lt;p&gt;Definitions like active user, conversion, or revenue are not implementation details. They are business concepts, and they change over time. When those concepts aren’t modelled explicitly, they drift - silently and dangerously.&lt;/p&gt;

&lt;h3&gt;
  
  
  What First-Class Actually Means
&lt;/h3&gt;

&lt;p&gt;Treating analytics as a first-class domain doesn’t mean building a BI tool inside Rails.&lt;/p&gt;

&lt;p&gt;It means a few concrete things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Analytics concepts have names&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Those names refer to persisted definitions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Queries are built from those definitions, not the other way around&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Presentation is a consumer, not an owner&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, analytics becomes something the application knows about, not just something it happens to compute.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stability Enables Change
&lt;/h3&gt;

&lt;p&gt;This may sound counterintuitive, but making analytics more explicit usually makes systems more flexible.&lt;/p&gt;

&lt;p&gt;When datasets are named and stable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;charts can change without redefining data&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;reports can evolve without duplicating logic&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;performance strategies can be swapped without touching consumers&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;definitions can be versioned instead of rewritten&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The system gains leverage because fewer parts are responsible for knowing everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Familiar Rails Pattern, Applied Consistently
&lt;/h3&gt;

&lt;p&gt;None of this is foreign to Rails developers.&lt;/p&gt;

&lt;p&gt;We already do this elsewhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Controllers don’t own business rules&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Views don’t own persistence&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Models don’t own presentation&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Analytics just hasn’t traditionally been given the same treatment.&lt;/p&gt;

&lt;p&gt;Once you apply the same architectural discipline - clear responsibilities, explicit boundaries, stable concepts - analytics stops feeling like a mess of special cases and starts behaving like part of the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analytics That Survives the Second Rewrite
&lt;/h2&gt;

&lt;p&gt;Most Rails applications can ship analytics.&lt;/p&gt;

&lt;p&gt;The harder problem is keeping analytics correct, understandable, and changeable once the application has been around for a few years.&lt;/p&gt;

&lt;p&gt;That’s usually when teams discover that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;definitions have drifted&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;logic is duplicated in subtle ways&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;no one is quite sure which chart is “right”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;changes feel risky even when they shouldn’t be&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These problems aren’t caused by a lack of tooling. They’re caused by analytics never being given a clear place in the architecture.&lt;/p&gt;

&lt;p&gt;When analytics is treated as a first-class domain - with explicit concepts, stable definitions, and clear boundaries - it becomes easier to reason about and safer to evolve. Not because it’s abstracted, but because it’s named.&lt;/p&gt;

&lt;p&gt;This way of thinking is what led me to build &lt;a href="https://github.com/CodeTectonics/analytics-plane" rel="noopener noreferrer"&gt;AnalyticsPlane&lt;/a&gt; - a framework designed to make these boundaries explicit and durable in real Rails applications.&lt;/p&gt;

&lt;p&gt;If you’ve felt the pain of analytics code that grew organically and then hardened into something brittle, you’re not alone. And you don’t need to solve it all at once.&lt;/p&gt;

&lt;p&gt;Start by asking a simpler question: &lt;strong&gt;Where should analytics logic live in a long-lived Rails app?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sometimes, the answer isn’t “a better query” - it’s a better place for the concept to exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Final Thought
&lt;/h2&gt;

&lt;p&gt;None of this is intended as a universal solution. It’s one way of giving analytics a clearer place in applications that are expected to evolve over time.&lt;/p&gt;

&lt;p&gt;I’m interested in how others think about these trade-offs, particularly in long-lived Rails systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Where does this kind of explicit structure start to feel like over-engineering in your experience?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;At what point in a Rails app’s life would you expect these boundaries to become worth the cost?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What analytics responsibilities would you be most tempted to collapse - and why?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>analytics</category>
      <category>architecture</category>
      <category>rails</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Decorator Dryer: Keeping Your Ruby On Rails Decorators DRY</title>
      <dc:creator>Mark Harbison</dc:creator>
      <pubDate>Sun, 14 Jan 2024 15:26:46 +0000</pubDate>
      <link>https://dev.to/markharbison/decorator-dryer-keeping-your-ruby-on-rails-decorators-dry-3jgi</link>
      <guid>https://dev.to/markharbison/decorator-dryer-keeping-your-ruby-on-rails-decorators-dry-3jgi</guid>
      <description>&lt;p&gt;As Ruby on Rails developers, we often find ourselves striving for clean, concise, and maintainable code. The "Don't Repeat Yourself" (DRY) principle is at the core of our coding ethos, yet when it comes to decorator classes, we sometimes encounter the inevitable build-up of repetitive code. This contradiction can make maintaining and updating decorators a challenging task.&lt;/p&gt;

&lt;p&gt;Decorators play a crucial role in Rails applications by encapsulating presentation logic, but as our codebase grows, so does the list of attributes that we need our decorators to transform. In our pursuit of cleaner code, leveraging tools like Draper has been a game-changer. Draper simplifies decorator implementation and enhances the maintainability of our code. However, even with Draper, repetitive patterns can emerge within decorators, compromising the DRY principle we hold so dear.&lt;/p&gt;

&lt;p&gt;Enter Decorator Dryer, an outstanding companion gem for Draper that addresses these challenges head-on. This gem streamlines the decorator writing process, keeping them DRY and easy to manage, thereby enhancing the efficiency of our Rails applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem In More Detail
&lt;/h2&gt;

&lt;p&gt;Take dates as an example. Formatting dates is a very easy thing for a decorator to do. Just choose a date attribute, choose a format, and make a call to &lt;code&gt;strftime&lt;/code&gt;. But what if your decorator needs to do this for 10 date attributes? You now have 10 almost identical methods in your decorator.&lt;br&gt;
Even if you centralise your formatting logic into a reusable method, you still have to write 10 methods that call that method, each passing in a different attribute.&lt;/p&gt;

&lt;p&gt;With Decorator Dryer, this is a thing of the past.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Choose Decorator Dryer?
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Seamless Integration with Draper
&lt;/h3&gt;

&lt;p&gt;Decorator Dryer seamlessly integrates with Draper, enhancing its capabilities by reducing redundancy in decorator classes. By using Decorator Dryer alongside Draper, you ensure a unified and efficient approach to managing your decorators.&lt;/p&gt;
&lt;h3&gt;
  
  
  Code Reduction and Improved Maintainability
&lt;/h3&gt;

&lt;p&gt;One of the key benefits of Decorator Dryer is its ability to significantly reduce repetitive code within decorators. It extracts common decorator functionalities into reusable methods and leverages the power of meta-programming to eliminate the need for repetitive calls to those methods, leading to cleaner and more maintainable codebases.&lt;/p&gt;
&lt;h3&gt;
  
  
  Faster Development of Decorators
&lt;/h3&gt;

&lt;p&gt;By eliminating redundant code and providing a structured way to organise shared functionalities, Decorator Dryer accelerates the development process. Developers can focus more on the unique aspects of their decorators, leading to faster iteration and deployment.&lt;/p&gt;
&lt;h2&gt;
  
  
  Getting Started with Decorator Dryer
&lt;/h2&gt;

&lt;p&gt;Implementing Decorator Dryer into your Rails application is a straightforward process:&lt;/p&gt;


&lt;h3&gt;
  
  
  1. Installation:
&lt;/h3&gt;

&lt;p&gt;Add Decorator Dryer to your application's Gemfile.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gem 'draper'
gem 'decorator_dryer'
And the execute:
bundle install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Inclusion:
&lt;/h3&gt;

&lt;p&gt;Add Decorator Dryer to your top-level Draper decorator.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class ApplicationDecorator &amp;lt; Draper::Decorator
  include DecoratorDryer
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Implementation:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class PersonDecorator &amp;lt; ApplicationDecorator
  to_date_format :date_of_birth, :date_of_graduation
  to_datetime_format :moment_of_birth
  to_time_format :lunch_time, :dinner_time
  to_precision_number :salary, :coffee_budget, precision: 2
  to_precision_number :height_in_meters, precision: 3
  to_attachment :profile_picture
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Decorate your models:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;date_of_birth = Date.new(2000, 1, 1)
date_of_graduation = Date.new(2018, 6, 6)
person = Person.create(date_of_birth: date_of_birth, date_of_graduation: date_of_graduation)
person.decorate.date_of_birth # ==&amp;gt; '2000-01-01'
person.decorate.date_of_graduation # ==&amp;gt; '2018-06-06'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Decorators are an essential part of Ruby On Rails applications, but they have a tendency to become overly repetitive, especially in larger application. For best results, keep the DRY.&lt;/p&gt;

&lt;p&gt;Decorator Dryer stands as a valuable tool in the arsenal of any Ruby on Rails developer seeking cleaner, more maintainable code. By working seamlessly together with Draper, reducing code repetition, and expediting development, it's a gem that truly lives up to its name.&lt;/p&gt;

&lt;p&gt;So, let's keep our decorators DRY and our codebases clean!&lt;/p&gt;

&lt;p&gt;Check it out on Github: &lt;a href="https://github.com/CodeTectonics/decorator_dryer" rel="noopener noreferrer"&gt;https://github.com/CodeTectonics/decorator_dryer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>rails</category>
      <category>codequality</category>
      <category>decorators</category>
    </item>
  </channel>
</rss>
