Skip to content

some updates for dev documentation - #10065

Open
ajtmccarty wants to merge 2 commits into
release-1.11from
ajtm-07282026-dev-docs-update
Open

some updates for dev documentation#10065
ajtmccarty wants to merge 2 commits into
release-1.11from
ajtm-07282026-dev-docs-update

Conversation

@ajtmccarty

@ajtmccarty ajtmccarty commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

trying to distill the changes from #10052 into something more repeatable

truly not sure if some of this might be better as dev/guides/backend/creating-new-components.md


Summary by cubic

Adds clearer backend dev docs on interfaces/protocols, configuration validation, and testing doubles. Uses the API backpressure work to show how to wire observers, contain failures, and keep third‑party deps out of core logic.

  • Documentation
    • Interfaces/Protocols: choose implicit Protocol next to the consumer or an explicit interface module; never put an ABC in the consumer; name methods in the consumer’s vocabulary; keep adapters in their own module; only the wiring layer imports both; verify no third‑party packages appear under the logic by grepping the import graph.
    • Configuration and invariants: tightly bound numeric fields (ge/le, allow_inf_nan=False); enforce cross‑field rules with @model_validator and name the rule; test the validator and shipped defaults; use enums for values that leave the process (e.g., metric labels) and pass enum members, not strings.
    • Testing: provide two doubles for injected collaborators — a recording double for exact call sequences/values, and a failing double to prove best‑effort paths survive collaborator failure; keep shared helpers and prefer adapters over patching.
    • API backpressure example: events are named methods; a single _notify fans out with per‑observer failure containment; wiring in server.py chooses sinks so api/admission logic stays decoupled behind Protocols.

Written for commit 380efeb. Summary will update on new commits.

Review in cubic

@ajtmccarty
ajtmccarty requested a review from a team as a code owner July 28, 2026 18:53

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 5 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Shadow auto-approve: would auto-approve. Updates only development documentation with structured guidelines for component design, configuration validation, testing doubles, and error handling. No behavioral, operational, or config changes.

Re-trigger cubic

@ogenstad ogenstad left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me overall, but added two comments.


Anything that comes from outside the component's own domain — loaded settings, an external service client, a telemetry sink — is resolved at that entry point, never inside the component:

- **Settings resolve in the factory, not the component.** A component takes plain values (`window_seconds: float`, `max_retries: int`), never a `Settings` object and never a module-global read. The factory is then the only place that knows a value came from configuration, which is also what makes the component directly testable with hand-picked values.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any examples of this? In situations where there's a high number of settings i thing a Settings object can be cleaner. An example would be the Config object for the SDK.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the Config is a special case b/c it is a settings object. its only responsibility should be tracking many settings. any components that actually do things should have settings injected when they are constructed.

for observer in self._observers:
try:
observer.on_depth_changed(queued=queued, running=running)
except Exception:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should include except Exception as a good example. (https://docs.astral.sh/ruff/rules/blind-except/)

Instead we should have a specific exception even if it's made up so that the LLM doesn't pick up on this part and start to introduce additional broad exceptions. We also have #10002.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in the case of an "observer" object (or any other "best-effort side-effect" components that should not block the primary logic during failure) we want to catch the bare Exception. we could move the bare Exception catch into on_depth_changed in this example and re-raise a different error class, but the end result would be the same

@ajtmccarty
ajtmccarty changed the base branch from develop to release-1.11 July 29, 2026 18:19

- **Use a `Protocol`, not an ABC, and declare it beside the component that depends on it.** Structural typing is what makes this work: the adapter satisfies the protocol by having matching signatures, so it never imports the protocol's module and the dependency does not run backwards. An ABC does not have that property — the adapter must subclass it, and therefore import whatever module it lives in. If you genuinely need an ABC, it goes in a module of its own that both sides import, never in the consumer's module.
- **Name the methods in the depending component's vocabulary**, not the adapter's — `on_depth_changed(*, queued: int, running: int)`, not `set_gauges(...)` — and pass the values as arguments rather than handing over `self`, so the adapter can never read back into the component. The component then depends on a shape it defined, has no idea what is on the other side, and stays free to change its internals.
- **Put the concrete adapter in a separate, purpose-named module** that is the only place importing the library, and **let only the wiring layer import both** (see "Build components near the application entry point").

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there anything worth saying about explicitly declaring the protocol in the class definition?

I mean this:

class MyClass(MyProtocol):
    # Protocol implemented

And not:

class MyClass():
    # Protocol implemented implicitly

The upside of the first option is the direct link between the implementation and the protocol it satisfies. The downside is that we would need to import the protocol. Another option would be to make sure these protocols live in a package that is safe to import from anywhere.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm always ready to say more about dependency injection. added some more detail to describe either using a Protocol without importing it or using a Protocol/ABC interface defined in a separate module

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Shadow auto-approve: would auto-approve. The PR adds only documentation (backend design, testing, configuration guidelines). No behavior, configuration, API, or operational change exists. The diff is bounded and clearly beneficial.

Re-trigger cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants