When you need an RFC

Free preview 5 min to read

An ADR records a decision that was made. An RFC is how a decision gets made when it cannot be made in one person's head.

This repository has one human contributor and 3,834 commits, so it has never needed one. That is the honest starting point, and it is also why the trigger conditions are worth knowing precisely. An RFC process adopted before it is needed is pure ceremony, and one adopted after it is needed arrives as a reaction to something that already went wrong.

The four triggers

More than one reasonable answer. If there is one obvious approach, write it and open the pull request. An RFC is for a fork in the road, not for a road.

The decision affects work other people are doing. A change to a service object three teams call is not yours to make alone, regardless of how right you are.

It is expensive to reverse. A schema change, a public interface, a dependency. The ownership course's reversibility table applies directly: the bottom three rows deserve an RFC and the top three do not.

Someone has already objected. A pull request with a disagreement in the comments has become an RFC whether anyone called it one. And it is going to be conducted in the worst possible venue (a diff) unless someone moves it.

One trigger is enough. Two makes it obvious.

flowchart TB
  Q["a technical change"] --> A{"more than one reasonable answer?"}
  A -->|"no"| P["open the pull request"]
  A -->|"yes"| B{"affects others' work, expensive to reverse, or already objected to?"}
  B -->|"no"| P
  B -->|"yes"| R["write an RFC"]

The subject for this course

One real, arguable question in this repository, which lesson 3 turns into an RFC.

app/queries/admin/stats_query.rb is the largest file in the application at 390 lines, and one of the most-changed at twelve commits. It contains twenty-three methods and returns a Data object with twenty-three fields.

Eight of those fields end in _chart_json, and this is what produces one of them:

def self.format_engagement_chart(trends)
  {
    labels: trends.map { |t| t[:label] },
    datasets: [
      { label: I18n.t("admin.dashboard.stats.enrollments"),
        data: trends.map { |t| t[:enrollments] },
        borderColor: "#007AFF",
        backgroundColor: "rgba(0, 122, 255, 0.05)",
        fill: true, tension: 0.4, pointRadius: 0,
        pointHoverRadius: 6, borderWidth: 2.5 },
      # ...and the same again for completions, in "#CC0000"
    ]
  }.to_json
end

Hex colors, borderWidth, pointHoverRadius, tension. And MEMORY.md states:

"Queries must return pure Data objects or Arrays of ActiveRecord Models. They are purely Domain operations and must never contain presentation logic, HTML formatting, or URL routing."

Why this is a good RFC subject and not a bug report

Because the code has a real reason to be this way, and reading it is what makes this an RFC rather than a correction.

Rails.cache.fetch("admin/stats/#{resolved}", expires_in: 5.minutes) do

The whole computation, including the chart JSON, is inside a five-minute cache. Moving the formatting into a helper would mean the colors and the serialization run on every dashboard request rather than once per five minutes.

That is a genuine argument, not a rationalization. Someone weighed a documented rule against a caching benefit and chose the caching benefit. And an RFC that pretends otherwise will be rejected by the person who wrote it: correctly.

All four triggers are present: there are at least three reasonable answers, the file is in a shared admin surface, a 23-field public Result is expensive to change. And a documented rule is already in tension with it.

What an RFC is not

Not an ADR. An RFC is a proposal, edited during review, with a decision at the end. An ADR is immutable and records the outcome. Lesson 6 is about how one becomes the other.

Not a design document. An RFC argues for a direction and names what it costs. A design document specifies how something works, and it comes after: or never, for a change this size.

Not a way to get permission. If the answer is obvious and you want cover for it, an RFC is a slow way to obtain what a pull request would have given you in an hour.

Not a place to think. Bring a position. An RFC opening with "what should we do about StatsQuery?" produces a thread rather than a decision, and lesson 5 is about why.

Key takeaways

  • An ADR records a decision; an RFC is how one gets made when it cannot be made in one head.
  • Four triggers: more than one reasonable answer, it affects others, it is expensive to reverse, someone has objected.
  • The subject here: a 390-line query returning eight _chart_json fields, against a documented rule forbidding presentation in queries.
  • It is an RFC and not a bug report because the code has a real reason: the whole computation sits in a five-minute cache.
  • An RFC is not an ADR, not a design document, not a permission slip, and not a place to start thinking.
Lesson 1 of 7 · free

Keep going with lesson 2

The shape of an RFC

The eight sections of an RFC, its length, where it lives, and why the decider is named up front.

A free account also gives you the quizzes, your progress and notes, and the discussion.

Type to start searching...