logic-sandwich
Logic Sandwich
Use this skill when a high-level module has too much mixed detail in one method and you want to preserve the module's role as an orchestrator.
Typical triggers:
- a top-level class method is reading input, making detailed decisions, and mutating collaborators all in one block
- you want to move detail out of a mediator/orchestrator without changing behavior
- a method should read more like "gather state -> decide intent -> apply intent"
- you are working in a deep-modules codebase where top-level files should stay simple and high-level
Goal
Refactor toward three layers:
- Top layer: gather state from the world
- Middle layer: compute a pure intent or action
- Bottom layer: apply the intent with side effects
The key idea is:
- top-level modules orchestrate
- middle logic decides
- lower layers mutate
Attribution:
- "Deep modules" is from John Ousterhout and fits the motivation here: keep top-level modules simple and high-level.
- "Logic sandwich" is from James Shore and fits the refactor shape here: gather state, compute intent, apply effects.
When To Use It
Use this when the method is an orchestrator over collaborators such as:
- controllers
- managers
- mediators
- handlers
- app-layer objects
Good fit:
- input handlers
- command handlers
- request/event handlers
- edit/session managers
Poor fit:
- leaf functions that are already pure
- tiny wrappers with no meaningful decision logic
- code where the "decision" is already embedded in a domain object with a good interface
The Shape
Target this structure:
function handleThing(input: Input) {
const state = gatherState(input);
const intent = decideThing(state);
applyThing(intent);
}
In practice:
- gather state inline if it is short and obvious
- extract the decision logic first
- keep the apply phase imperative and explicit
Workflow
- Identify the orchestration method.
- Mark which lines are:
- reading state
- deciding what should happen
- performing effects
- Extract the decision logic into a pure helper.
- Name the helper around intent, action, or plan:
decideInputIntentcomputeSavePlanderiveNavigationAction
- Replace inline branching with a small
switchor dispatch over the result. - Keep side effects in the orchestrator unless there is a clear second extraction boundary.
- Add narrow tests for the pure helper.
- Keep or add higher-level orchestration tests to prove integration still works.
Design Guidance
Prefer intent types over ad hoc booleans.
Better:
type InputIntent =
| { type: 'delete-current' }
| { type: 'insert-after-current'; insertedParts: string[] }
| { type: 'rewrite-current'; firstPart: string };
Worse:
let shouldMove = false;
let preferFirst = false;
let isWeirdCase = false;
The middle layer should answer:
- what does this situation mean?
The bottom layer should answer:
- how do we carry that out with collaborators?
Deep Modules Framing
In a deep-modules codebase, top-level files should be perusable without descending into implementation details.
This skill supports that by keeping the top-level module focused on:
- orchestration
- sequencing
- ownership boundaries
and moving detailed conditional logic into a smaller helper module underneath it.
A good result is that a human can open the top-level file and quickly see:
- what state is gathered
- what decision helper is called
- what collaborators are invoked
without reading every heuristic inline.
Testing Style
Prefer two layers of tests:
-
Pure helper tests
- narrow
- state-based
- cover the decision table
-
Orchestrator tests
- fewer
- prove the intent is applied correctly in the real flow
Do not replace orchestrator tests with only pure tests.
Heuristics
- First extraction should preserve behavior.
- Do not redesign semantics and refactor shape in the same step unless the tests are very strong.
- If a field exists only to support one branch, consider moving it to that intent variant.
- If a branch needs unusual post-processing, consider making it its own intent rather than forcing it through a generic path.
- If the apply phase starts to sprawl, you may do a second extraction:
applyInputIntent(...)But only if that makes the top-level method clearer.
Output Standard
When applying this skill:
- keep the orchestrator readable
- prefer a small pure helper in a lower-level module
- add or update narrow tests for the helper
- keep names concrete:
intent,action,plan,decision - document intent variants with concrete scenario notation when helpful
More from danielbush/skills
nullables
Guide for implementing James Shore's Nullables pattern and A-Frame architecture for testing without mocks. Use when implementing or refactoring code to follow patterns of: (1) Separating logic, infrastructure, and application layers, (2) Creating testable infrastructure with create/createNull factory methods, (3) Writing narrow, sociable, state-based tests without mocks, (4) Implementing value objects, (5) Building infrastructure wrappers that use embedded stubs, or (6) Designing dependency injection through static factory methods.
15work-tracker
Create and manage work items, tickets, and tracking artifacts in a project's work/ directory. Also handles session continuity, summarisation, and searching past work. Supports unsupervised tickets — self-contained work items queued for autonomous agent execution. Use when the human wants to: create/move/scan work items, review the backlog, summarise a session, recall past work, continue from a previous session ('where were we', 'let's continue'), or queue work for an unsupervised agent. Bootstraps work/ on first use.
15nullables-test
Write illustrative tests for code that follows the Nullables pattern. Verifies the class under test is ready (all HARDWIRED_INFRA replaced by INJECTED_INFRA, every dependency has .createNull), then writes narrow, sociable, state-based tests using .createNull(). Tests should illustrate the system's concepts and architecture, not just achieve coverage. Use after applying the nullables-refactor skill, or when writing tests for code that already uses DUAL_FACTORY.
13nullables-refactor
Analyze a file and produce a refactoring plan to apply the Nullables pattern. Classifies code by side-effect boundary (PURE, IN_MEMORY, OUTSIDE_WORLD), identifies HARDWIRED_INFRA, recommends INFRASTRUCTURE_WRAPPER or NULLABLE_CLASS conversion, checks DUAL_FACTORY and CREATE_BOUNDARY_RULE compliance, and decides on DELAYED_INSTANTIATION. Use when asked to refactor a file or module to follow the nullables pattern.
13effect-ts
>
7nullable-architecture
Use when refactoring code or writing tests in the Nullables style: choose between `new`, `.create()`, and `.createNull()`, introduce infrastructure wrappers at the environment boundary, add behavior simulation and output tracking, and write narrow, sociable, example-driven, state-based tests without mocks or spies.
5