River
Y CombinatorBacked by Y Combinator
FREE TEMPLATE

Engineering RFC and Design Doc Template

Three statuses cannot tell an unanswered objection from a decision nobody recorded. Eleven states, a decision date, and the count of comments nobody disposed of.

Free download  ·  No account needed

Search for an RFC template and you get nine headings and three statuses: draft, in review, approved. The headings are fine. The statuses are why proposals fail. Under three values, a proposal rejected on purpose, a proposal parked until next year, a proposal accepted eight months ago that nobody is building, and a proposal waiting on one colleague's unanswered objection all render as the same word. Nobody skips the format. What gets skipped is finishing the decision.

A status is three separate facts. Which state the proposal is in, from a vocabulary that tells a decided no apart from a parked maybe. When it stops being in that state, which is a date no template has. And whether an objection against it is unanswered, which is a count. Real processes carry all three. Kubernetes puts the state in a machine-readable file with seven permitted values, beside a field for the last milestone in which work actually happened.

This pack carries eleven states, a decision date on every row, and a comment log joined to the register rather than filed beside it. Kestrel Data's worked register holds 18 proposals. Five are live, and a three-state view calls all five in review; three are past their own decision date, one by 119 days. Six blocking comments sit open across four proposals. The oldest is 335 days old, nobody argued with it, and the engineer who raised it is still here.

Why has this not moved, answered with a number

Every blocking count is recomputed from the comment log. Nothing in that column was typed in by a person.

RFC Register

Kestrel Data, a fictional streaming analytics platform. Status On Document is the header transcribed. Status is the normalised state. Blocking is a join against the comment log.

RFCTitleHeader saysStatusDuePast dueSign-offsBlockingImpl. owner
RFC-014Shared partitioned topicIn ReviewFinal Comment Period2026-09-1135 of 51
RFC-017One ingest specificationIn ReviewIn Review2026-05-181193 of 42
RFC-009Query planner in RustIn ReviewIn Review2026-08-04412 of 41
RFC-015SDKs from the descriptionDraftIn Review2026-09-2801 of 40
RFC-013Jobs on the batch clusterDraftDraft2026-09-2101 of 40
RFC-006Tracing across every serviceIn ReviewDeferred2025-10-20n/a2 of 42Unassigned
RFC-016Drop Python 3.9In ReviewDeferred2026-03-02n/a1 of 40Unassigned
RFC-011Service mesh east-westOn holdPostponed2026-06-10n/a4 of 40
RFC-003One error envelopeApprovedAccepted2025-11-17n/a4 of 40Unassigned
RFC-012One flag evaluation serviceApprovedAccepted2026-06-15n/a4 of 40Unassigned
RFC-008Tiered storage past 30 daysApprovedProvisional2026-03-23n/a4 of 40P. Adeyemi
RFC-001Semantic versioningAcceptedImplemented2025-07-28n/a4 of 40L. Marchetti
RFC-004Deploy-time DB credentialsRejectedRejected2025-11-24n/a4 of 40n/a
RFC-005Jobs off the app clusterDraftWithdrawn2026-01-26n/a1 of 40n/a
RFC-018Retire internal metrics formatImplementedSuperseded2025-09-22n/a4 of 40n/a

18 rows, all 11 states represented. A three-state template renders 5 of these as In Review; here they read Draft, In Review three times and Final Comment Period, and 3 are past a date they set themselves. Nine headers name a different state than the register does, and every one of the nine flatters. Four more differ only in wording, where Approved normalises to Accepted and On hold to Postponed, and the sheet counts those separately.

Comment Resolution Log

One row per comment. A class chosen by the reviewer before they type, then a disposition that requires a person, a date and where it was written down.

IDRFCConcernClassDispositionDays openChanged design
C-011RFC-006Instrumenting every service needs an owner per service and there is no listBlockingOpen335
C-012RFC-006The sampling decision belongs at the edge, not inside each serviceBlockingOpen328
C-031RFC-017One specification for two API versions needs a stated deprecation pathBlockingOpen139
C-032RFC-017Generated reference would replace the handwritten guides nobody ownsBlockingOpen131
C-021RFC-009No number here says what the current planner costs, so there is nothing to beatBlockingOpen84
C-045RFC-014The migration needs a rollback that does not lose in-flight eventsBlockingOpen31
C-022RFC-009Two engineers can maintain the current planner and one of them is leavingNon-blockingOpen76
C-041RFC-014A shared topic lets one tenant's replay throttle every other tenant's ingestBlockingAccepted, design changed10Yes
C-042RFC-014Per-tenant retention becomes impossible once the topic is sharedBlockingAccepted, design changed10Yes
C-043RFC-014Partition count cannot be reduced later, so the first number is a one-way doorBlockingDeferred to a follow-up13No
C-044RFC-014Call it a shard rather than a partition, to avoid the broker's own termNon-blockingDeclined with reason2No
C-046RFC-014Does this change what the client library has to do on reconnectQuestionAnswered, no change needed1No
C-002RFC-004Even revised, the rotation window is longer than the incident it should containBlockingAccepted, RFC rejected6No

16 comments in the shipped sheet: 12 blocking, 3 non-blocking, 1 question. Six blocking are still open, and they are the register's whole blocking count. C-011 was raised by the decider, nobody argued with it, nobody produced the list, and the author of RFC-006 left the company in March. C-045 is the expensive shape: the operability reviewer had already signed off, the migration section landed afterwards, and the objection arrived three days later while nobody was looking. Five comments changed a design, which is the healthiest number on the sheet.

Review Sign-off

Position and depth are separate columns, because signing off and reading are separate events.

RFCReviewerRoleRequiredPositionDepthNote
RFC-014R. VillalobosOperabilityYesSigned offRead in fullSigned off, then raised C-045 once the migration section landed
RFC-014T. BergqvistQuery ownerYesSigned offRead in fullSigned off with C-043 deferred to RFC-019 by agreement
RFC-017H. NakagawaStorage ownerYesSigned offSkimmedRecorded as skimmed at the reviewer's own request
RFC-017N. VasquezDeciderYesSigned offRead in fullRaised C-031, still open
RFC-017R. VillalobosOperabilityYesNot lookedNone
RFC-006J. OkonkwoAuthorYesSigned offRead in fullAuthor. Left the company in March 2026
RFC-006S. OyelaranSecurityYesNot lookedNone
RFC-015H. NakagawaStorage ownerNoNot requiredn/aRecorded as not required, so the empty row is a ruling rather than a gap
RFC-013N. VasquezDeciderYesNot lookedNone

26 rows in the shipped sheet: 14 signed off, 11 not looked, 1 ruled not required. Depths run 13 read in full, 1 skimmed. RFC-014 reached a final comment period because 5 of 5 signed off, which is the gate Rust's process describes, and it is still sitting there with a blocking comment open. RFC-006 shows the other failure: two of four required reviewers never opened it, and for eleven months its header said In Review, so nobody read that as a problem.

What comes in the pack

01

RFC Register, with the header as written beside the state that is true

One row per proposal. Status On Document holds what the document's own header literally says, transcribed. Status holds the normalised state from the eleven-value vocabulary. They disagree on nine of the eighteen worked rows and the disagreement always flatters: Accepted on something that shipped last autumn, In Review on something nobody has opened since spring, Draft on a document four people have reviewed. Four more rows differ only in wording, and the sheet counts those separately because they are a different problem.

02

A decision date on every row, and the days past it

In Review with no end is where proposals go to die, and no RFC template has this field. Set it when you open the document, two weeks inside a team and six across three, and name one decider rather than a group, because a committee cannot be past due. Three of the five live rows in the worked register are past theirs, by 3 days, 41 days and 119 days, and the register says so on the row rather than in somebody's memory.

03

Comment Resolution Log, joined to the register

One row per comment, each with a class and a disposition. Blocking, non-blocking or question, decided by the reviewer before they type. Then one of six dispositions, each requiring a person, a date and where the resolution was written down. The register's blocking count is a join against this sheet, not a field, so it cannot drift from the thing it counts.

04

Review Sign-off that records depth, not just position

Signed off, not looked, blocked, or ruled not required with the ruling attached, so an empty row is a decision instead of a gap. Beside it, whether the reviewer read the proposal in full or skimmed it. That is not an accusation: Rust's own process notes the sign-off gate before a final comment period is often where reviewers first read a proposal in full depth. Recording the skim is what makes the fraction honest.

05

A rule that stops a state advancing over an open objection

While a proposal has a blocking comment with no disposition, its state cannot move toward a decision. It can move away from one, because Deferred and Withdrawn are honest descriptions of a stalled proposal. The worked register ships one row breaking the rule on purpose, and keeps it visible rather than tidying it away, because a register that cannot show a violation hides the thing you needed it for.

06

A monthly sweep, and the decision record it feeds

Four passes: past their date, gone silent, accepted and unbuilt, header disagrees with the register. It reports arithmetic before it reports a list, and it proposes state changes rather than applying them, because the name in Status Set By has to belong to somebody who chose it. Every resolution writes an entry naming what was decided, who decided it, and where, which is what an architecture decision record starts from. Thirteen of the eighteen worked proposals have one.

How it works

  1. 1

    Send whatever you already have

    A proposal you are drafting, a problem you want to change, or the folder somebody started two years ago. River reads the review threads too, because that is where the unanswered objections are. It pulls constraints from an architecture document, or reconstructs one when the system was never written down.

  2. 2

    Every proposal gets a state, a date and a count

    The header is transcribed, then the state is set from evidence rather than from the header. Live rows get a decision date. Every substantive concern in the threads becomes a classified comment, and the blocking count follows.

  3. 3

    The first report is arithmetic

    How many proposals your team calls in review, how many are past a date, how many blocking comments are open and what their proposals' statuses claim. Then the three rows that need a person today, each with the reason.

  4. 4

    Draft, review and resolve inside the register

    Five prompts open a proposal, review one from a named role, dispose of a comment, sweep the stalled rows and record the decision. Each recomputes the counts, so the sheets never drift. A resolved proposal is the agreed design a threat model reads, and the change it authorises is what a migration plan sequences.

Frequently asked questions

Why eleven states instead of three?

Because the four things three values cannot tell apart are the four things that go wrong. Every state here is one a shipped process carries. Kubernetes' own proposal metadata permits seven values in a machine-readable file, and separating Deferred from Postponed matters because one means nobody is working on this and the other means we chose to wait.

Is Deferred just a polite rejection?

No, and the distinction is documented. Python's process defines the status by one test: an author or editor assigns it when no progress is being made. It is a fact about the document, not a verdict on the idea. Applying it is a kindness, because the alternative is a register claiming five things are in review when two of them are not.

What stops an accepted RFC from never getting built?

Two columns and a sweep pass. Accepted rows carry an implementation owner and a tracker, and blank ones are reported. Rust's process makes the tracking issue mandatory for the same reason, and settles ownership too, since the author of a proposal is not obliged to implement it. Two of the eighteen worked rows are accepted with nobody named.

How does the blocking count actually work?

It is a join, recomputed rather than typed. It counts the comment log's rows against that proposal whose class is blocking and whose disposition is Open. Six are open in the worked register, and the four proposals holding them read In Review, In Review, Final Comment Period and Deferred. That is the field which separates a proposal stalled by disagreement from one stalled by indifference.

Does this replace an architecture decision record?

No. An architecture decision record holds a decision already taken and answers whether it is still in force. This pack holds the interval before that, when the proposal is still being argued about. Choosing between named candidate technologies first is a separate evaluation, in the technology selection pack.

Why record that a reviewer only skimmed it?

Because a sign-off given without a full read is a normal event, and pretending otherwise makes the whole sheet worthless. One worked row is marked skimmed at the reviewer's own request. Depth also explains the failure mode where somebody signs off, the migration section lands afterwards, and their blocking comment arrives three days later.

What if my team's proposals are scattered across a wiki?

That is the normal starting state and the first pass handles it. River builds the register from whatever it finds, transcribes each header, sets the real state from evidence, and classifies the old review threads into comments. Expect the two status columns to disagree on about half the rows, and expect the direction to be flattering.

Find out which of your RFCs is dying

Send the folder. River puts every proposal on the register with its real state, its decision date, and the objections nobody answered.

Install the RFC pack