--- name: design-first description: Shape non-trivial work before writing it — decide the modules, the seams, and the public API surface up front. Use when starting a feature, adding an integration, changing a public barrel file, crossing a package or native-interop boundary, or deciding where a seam should go. Produces a design sketch to approve before implementation, then hands off to code-guidelines and test-guidelines. --- Design the shape before writing the code. The pass ends in a **design sketch** the user approves; implementation does not start until it does. Aim for **deep modules** placed at clean **seams** — so callers get leverage, maintainers get locality, and tests get a surface to push against. This is a design pass, not an implementation plan. Decide *what shape the code takes and why*; leave the line-level rules to code-guidelines and the test structure to test-guidelines. ## When to run Run for work where the shape is a real decision: - A new feature or a new `Integration` - A change to a public barrel file (`lib/sentry.dart`, `lib/sentry_flutter.dart`) — it cascades to every downstream package and user - Work that crosses a package boundary or the native-interop (JNI/FFI) boundary - Any moment you are choosing where a seam goes, or how a module is shaped to be testable Skip it — and say you are skipping it — for one-line fixes, localized bug fixes with obvious placement, and refactors that change no interface. ## Vocabulary Use these terms exactly in the sketch and the discussion — consistent language is what makes the design legible across sessions. Don't substitute "component," "service," "layer," or "boundary." - **Module** — anything with an interface and an implementation: a function, class, or package. - **Interface** — everything a caller must know to use it correctly: the signature, plus invariants, ordering, error modes, and required config. - **Deep module** — a lot of behavior behind a small interface. The goal. A **shallow** module has an interface nearly as complex as its implementation — avoid it. - **Seam** — the place where you can swap behavior without editing in that place. Where a module's interface lives, and where a test double crosses. - **Adapter** — a concrete thing satisfying an interface at a seam. Two checks settle most design questions: - **The deletion test.** Imagine deleting the module. If complexity vanishes, it was a pass-through — don't build it. If complexity reappears across N callers, it earns its keep. - **The interface is the test surface.** Callers and tests cross the same seam. If you need to test *past* the interface, the module is the wrong shape — and per test-guidelines, tests through a clean interface survive refactors. ## The design pass ### 1. Frame the change State the behavior the change introduces, in the domain's own words. Name the package(s) it lands in. Read the relevant `AGENTS.md` (root, `packages/dart/`, `packages/flutter/`) for the area you're touching. **Find the nearest well-shaped precedent and align to it.** The codebase has almost certainly solved something adjacent; locate its *best* current example and match it — or improve on it deliberately, noting why. The repo's own best-organized case is a stronger guide than any rule (e.g. follow `telemetry/span/` v2, not the v1 span scatter). This auto-updates as the codebase evolves. ### 2. Shape the modules Decide the modules and where their seams go. Prefer fewer, deeper modules over many shallow ones. For each module, name its interface — not just the signature, but the invariants and error modes a caller must know. Run the deletion test on anything you suspect is a pass-through. Decide where the code lives as part of shaping it — a **locality** call: co-locate what changes together so one change stays directory-local. The discriminator is **shared vs owned**: - A cohesive subsystem owns its model, lifecycle, and pipeline as one **concept** — group it together, sized to the change (a big subsystem earns its own dir; a small addition matches the surrounding convention). - Peel a piece into a **shared** module only when it has its own lifecycle *and* more than one consumer — the deletion test decides it (would extracting it concentrate complexity, or just move it?). - **If you can't name the concern a piece serves, that's a smell** — it's doing two things, or belongs to a concept you haven't named yet. A cross-cutting concern (enrichment, exceptions) is still a concept with a home; a truly nameless one means the design is off. For the repo's concrete layout — which dirs are layer vs concept, the `native/` seam exception, the legacy loose tier — see code-guidelines' **File Organization**. ### 3. Design for testability A seam exists so a test can cross it. For each module, name the seam its tests will cross and what fake injects there (test-guidelines builds these via `Fixture` + `getSut()`). Three rules make that possible: - **Accept dependencies, don't create them.** A module that constructs its own transport or client can't be faked; one that receives it can. - **Return results, don't bury side effects.** A function that returns a value is testable; one that only mutates shared state is not. - **Keep the surface small.** Fewer methods and params mean less test setup. Native interop is special: **JNI/FFI cannot be faked or mocked** (see `packages/flutter/AGENTS.md`). Put the seam at the Dart boundary *above* native so the logic is unit-testable, and plan an integration test for the native path itself. ### 4. Check the SDK constraints Resolve these before sketching — each has a canonical home; consult it rather than guessing: - **Public API surface.** Does this add or change exports in a barrel file? Keep new types in `src/` unless they're meant for SDK users. See `packages/dart/AGENTS.md`. - **Integration shape.** If the feature is an `Integration`, it implements `call()`/`close()`, gates on its prerequisites early, and stays order-independent. See code-guidelines. - **Package boundary.** Core behavior belongs in `packages/dart/`; integration-specific behavior in its own package. A new dependency in core cascades everywhere. ### 5. Write the design sketch and get approval Write a short sketch — prose or bullets, not code — and present it. **Do not start implementing until the user approves it.** The sketch is complete when it states, for the change: - [ ] Each module, its interface, and whether it's new or modified - [ ] The seam each module's tests cross, and the fake that injects there (or "integration test" where native) - [ ] The public-API-surface impact (barrel-file exports added/changed, or "none") - [ ] One alternative shape you considered and why you rejected it If the solution space is wide and the best interface isn't obvious, design it twice before sketching — see [references/design-it-twice.md](references/design-it-twice.md). ### 6. Hand off On approval, implement under code-guidelines, and write tests under test-guidelines at the seams this sketch named.