Skip to main content
The BlockX repository uses three design principles to constrain both specs and code. They are not abstract slogans: the repository’s AGENTS.md and docs/specs/code-style.md expand them into concrete module boundaries, dependency rules, error semantics, and time semantics. This page is a reader’s guide to those two files, so that you know what will get sent back in review before you write any code.

Three principles

A new subsystem first defines a pure in-memory core that describes its behavior with explicit events and commands, and only then wraps adapters around it. The core only maintains the state machine, idempotency decisions, phase advancement, and result convergence; RPC, UDS, etcd, databases, clocks, logging, and metrics all stay in adapters.WorkerCore (internal/worker/core/), CoordinatorCore (internal/coordinator/core/), and DispatcherCore (internal/worker/core/dispatcher.go) in the repository all take this shape. They are code boundaries, not new processes or roles.
Modules such as IO access, which revolve around who owns a resource and when it is released, are not forced into an event/command stream. They explicitly model a Worker-level shared scope and task-level local scopes, and bind quota, cache, singleflight, deadline, and cleanup to the scope lifecycle. Scope exit is a structural cleanup boundary; it does not rely on “sending a release command later”.internal/io/ takes this shape; its entry points are synchronous interfaces such as WorkerIOScope / TaskIOScope.
Do not multiply entities beyond necessity. Before adding a struct, interface, error code, queue, state, or service, first ask whether an existing concept can be reused or simplified. If you cannot clearly explain why the existing module / core / adapters / scope boundaries cannot accommodate a new entity, do not add it yet.
The rule of thumb for choosing between the principles comes from AGENTS.md: if a behavior is lifecycle-heavy (state advancement of slots, tasks, and calls), express it with explicit states, events, and commands; if it is resource-governance-heavy (connection pools, quotas, caches), express it with explicit scope ownership, acquisition, and cleanup rules; if neither framework yields a small, enumerable set of rules, the design is still too vague.

Layering and dependency rules

Do not introduce catch-all directories such as pkg/utils, common, or shared without clear boundaries. The existing internal/common/ in the repository only holds a few small packages with a clear purpose — argspool, clientid, types, and unsafebytes — and is not a general-purpose junk drawer.

Interaction pattern between adapters and the core

The recommended pattern is the one-way “input event → core decision → adapter executes commands”, rather than having the core hold a set of reverse callbacks. The sketch from docs/specs/code-style.md:
When a timer, Executor, Plugin, or IO callback completes, the adapter wraps the result back into a core event and feeds it in again. This keeps the core single-threaded, replayable, and testable in a table-driven way. For the exact event and command names, refer to the individual component pages: Worker, Coordinator, and Call execution subsystem.

Using interfaces

  • Define interfaces on the side that actually consumes them.
  • When adapters call the core, there is usually no need to abstract an interface for the core; depend directly on concrete types such as WorkerCore and CoordinatorCore.
  • Only when the core genuinely needs a synchronous port, and that port can stay pure, narrow, and replaceable, define a minimal interface in core/ for adapters to implement.
  • Do not extract interfaces ahead of time for “possible future extension” before there is a second implementation or a clear need for a test double.

Naming and modeling

  • Package names are short, stable, and domain-specific; prefer nouns.
  • Use clear names for exported identifiers; avoid weak suffixes such as Manager, Util, Data, and Info.
  • States, events, and commands reflect domain semantics directly, for example TaskPhaseFinished, PrepareTaskActivation, and ReleaseSlot.
  • Prefer explicit structs and named enums; do not overuse map[string]any, any, or scattered string literals.
  • When you need a combination of several boolean fields to understand an object’s state, switch to an explicit state enum.
  • Use NewXxx only when it establishes invariants; a literal wrapper does not need a forced constructor.

Error and result expression

  • First distinguish “admission / protocol errors” from “terminal results after execution has started”; do not mix them into one set of error returns. On the Worker, the boundary is whether the slot has been activated from ALLOCATED to RUNNING: errors before activation are submission / admission-layer errors, and failures after activation all go into TaskResult.
  • Stable semantics rely on error codes, enums, or named result types, not on error strings.
  • When an external protocol already has stable error code conventions, the protocol fields are authoritative; do not invent parallel semantics inside adapters.
  • In a Sans-IO core, model expected business outcomes as explicit results; do not overuse error.
  • Boundaries such as TaskResult, InvalidSlot, NoSlot, and Unavailable must be consistent with docs/specs/architecture.md and docs/specs/worker.md.

Time semantics

  • Model time semantics centrally; do not scatter them across handlers, Plugins, and drivers, each making its own judgment.
  • On the Worker side, slot TTL, task deadline, and the result retention window are concentrated in the Worker module and its timer adapter, with docs/specs/worker.md as the authority.
  • On the Coordinator side, view freshness, candidate retries, and placement-related time semantics are concentrated in the Coordinator module, with docs/specs/task-resource-coordinator.md as the authority.
  • In scope-oriented modules, the end of a lifecycle is guaranteed primarily by scope Close(), context cancellation, and structural cleanup, supplemented by defenses against late results.
  • The creation, exit, and reclamation paths of goroutines must be clear; do not leave lifecycle management to process exit as a fallback.

Keeping tests and docs in sync

  • core/ tests focus on state transitions, idempotency, TTL, deadline, and failure convergence; prefer table-driven tests.
  • Tests for scope-oriented modules focus on quota acquisition / release, scope closing, dropping late results, singleflight behavior, and the absence of resource leaks.
  • Adapter tests focus on protocol mapping, error translation, and command execution orchestration; do not repeat the state combinations already covered by the core.
  • When you change protocol or lifecycle semantics, update the tests and the corresponding spec in the same change.
See Testing for the exact commands and test layering.
  • AGENTS.md in the blockx repository: repository-level principles, directory skeleton, test baseline, commit / PR rules.
  • docs/specs/code-style.md in the blockx repository: the source this page expands on, including the api/ boundary and the concrete constraints of Sans-IO and Scoped Resource Context.
  • docs/specs/io-subsystem.md in the blockx repository: the complete definition of Scoped Resource Context.
  • Repository layout and code layering
  • Contributing