| status | accepted |
|---|---|
| date | 2026-04-16 |
| decision-makers | Serghei Iakovlev |
The TrackerAdapter interface has 9 methods: 6 read operations, 2 core write operations
(TransitionIssue, CommentIssue), and 1 escalation write operation (AddLabel). As
the roadmap anticipates additional tracker write methods (RemoveLabel, AddAssignee,
ChangeStatus), the question is whether the single-interface pattern will become a
maintenance burden that violates Go's small-interface convention, or whether splitting
introduces unnecessary complexity for the current scale.
- Compile-time safety. All adapter implementations are internal to this repository. The compiler should catch every missing method at build time, not at runtime via type assertions.
- Orchestrator simplicity. The orchestrator calls tracker methods unconditionally. Introducing type-assertion branches adds conditional logic, fallback handling, and additional test paths at every call site.
- No-op cost. When an adapter does not meaningfully support a method, it returns
nil. The cost of one no-op stub per adapter per method is trivial at current scale. - Go interface cohesion. Interface size is justified when methods form a cohesive behavior contract. All 9 methods describe "what an issue tracker can do for the orchestrator" — a single responsibility.
- Keep
TrackerAdapteras a single unified interface - Split into
TrackerAdapter(core read/write) +TrackerEscalation(optional capability discovered via type assertion)
Chosen option: Keep TrackerAdapter as a single unified interface, because the cost
of splitting exceeds the cost of the current pattern at the present scale.
Only 1 of 9 methods is a no-op in 1 adapter (FileAdapter.AddLabel). Splitting would
require type-assertion branches at 2 orchestrator call sites, trading compile-time safety
for runtime discovery — a trade that is unnecessary when all adapters are internal and
every implementation is under our control. The 10 test doubles each gain one stub line per
new method; saving approximately 3 stubs does not justify the structural change.
Revisit this decision when all three conditions are true simultaneously:
- The interface exceeds 12 methods.
- At least 3 methods are no-ops in 2 or more adapter implementations.
- A concrete new adapter is being built that genuinely cannot support the no-op methods (e.g., a read-only tracker where write methods would violate the adapter's contract rather than being harmless no-ops).
Until these conditions are met, the unified interface is the simpler and safer choice.
Split into core + optional escalation interface. Under this option, escalation methods
(AddLabel, future RemoveLabel, AddAssignee) would move to a TrackerEscalation
interface. The orchestrator would discover the capability via type assertion
(if esc, ok := tracker.(TrackerEscalation); ok { ... }). This pattern is appropriate
when adapters are external and the set of implementations is open. For Sortie, all
adapters are internal, the set is closed, and the type-assertion branches add complexity
without benefit. Each call site gains a branch, a log line, and a test path for the
"not supported" case — producing the same outcome that today's no-op nil return achieves
with zero branching.