method-decomposition
Skill0 starsChanged 6 months agoMITView on GitHub
Use when designing, sizing, or refactoring a method/function in Java, Kotlin, or TypeScript — deciding whether to extract a helper, whether a method is too long or too short, or how to decompose a service, use-case, controller, WebFlux pipeline, or SQS/Kafka consumer. Apply when method length, cognitive/cyclomatic complexity, nesting depth, parameter count, or single-responsibility comes up, or a linter flags it (SonarQube S138/S3776/S1541/S107, Checkstyle MethodLength/ParameterNumber). Not for merely implementing a known business rule, fixing a bug, renames, or trivial CRUD/DTO edits — only when the method's shape or size is itself the question. Triggers on "should I extract this", "this method is too long", "is this over-engineered", "decompose", "split this method", "SLAP".
In this repository4 other files it gives its agents.
The file
Show its rules and text
We split this skill into the rules it gives an agent
13 rules. About 0 are common, 7 are distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈7
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 19
When a method's shape or size is the question, work through these steps in order:
- Very specific: line 22
Measure before judging (Java, on demand): If complexity is the real question and the method nests lambdas/streams/switches, run …
- Distinctive: line 23
Compare against the limits: Check the method against the *Concrete limits* table (length, cognitive/cyclomatic complexity, nesting, …
- Very specific: line 25
Respect stack boundaries: Before moving code, apply the stack-specific notes — @Transactional self-invocation, WebFlux chain stages, …
- Distinctive: line 26
Self-check: Run the *Self-check before submitting* list on each method you changed.
- Very specific: line 55
Run the bundled script (it self-locates assets/pmd-ruleset.xml and skips cleanly if PMD is absent):
- Distinctive: line 84
Do NOT extract when:
- Very specific: line 102
Never extract logic out of a @Transactional method into a new @Transactional method on the same class and call it via this.x() expecting a …
- Very specific: line 120
Keep them thin; never put business logic in @Query methods or in JdbcTemplate callbacks beyond mapping.
- Distinctive: line 163
Before writing code for a feature, run a Context7 lookup (and web search if needed) when ANY of these apply:
- Very specific: line 176
Call resolve-library-id then get-library-docs (Context7 MCP) for the specific topic, and/or a focused web search for blog posts / conf …
- Distinctive: line 187
If unsure, default to a 60-second Context7 check rather than guessing, but never burn a full research cycle on a one-line fix.
- Distinctive: line 191
Run mentally on each method you wrote/changed:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Method Decomposition
Overview
Methods exist to be read, not to be short. Optimise for a reader who has never seen the file. This skill is the full reference: the thresholds for the common case, the nuanced cases (when NOT to extract, stack-specific boundaries), and the sources to cite when justifying a decomposition decision.
Core principle (SLAP — Single Level of Abstraction Principle, Beck/Martin): a method does one thing at one level of abstraction. Cohesion over line count.
Fowler's heuristic (Refactoring, 2nd ed., p. 110): "the key is the semantic distance between the method name and the method body. If extracting improves clarity, do it, even if the name is longer than the code you have extracted." The corollary is also true: if it does NOT improve clarity, do not extract.
Workflow
When a method's shape or size is the question, work through these steps in order:
- Locate the decision point. Identify the specific method you are writing, expanding, or being asked to split — this guidance applies per method, not per file.
- Measure before judging (Java, on demand). If complexity is the real question and the method nests lambdas/streams/switches, run
scripts/measure-complexity.sh <file.java>for real cognitive/cyclomatic numbers instead of guessing. Skip for Kotlin/TS or when PMD is absent — estimate by reading. - Compare against the limits. Check the method against the Concrete limits table (length, cognitive/cyclomatic complexity, nesting, parameters). Treat every breach as a smell to investigate, not an automatic failure.
- Decide extract vs. inline. Apply When to EXTRACT and When NOT to extract. Extract only when the name encodes a distinct level of abstraction or domain rule; never to satisfy a line count.
- Respect stack boundaries. Before moving code, apply the stack-specific notes —
@Transactionalself-invocation, WebFlux chain stages,@Service/ repository / SQS layering — and never reorder I/O calls as a side effect of decomposition. - Self-check. Run the Self-check before submitting list on each method you changed; fix anything that fails the step-down / single-abstraction test.
Companion skill — java-flow-guideline: different altitude, not a rival. That skill decides
where an artifact belongs (which layer/package) and how to build and test it; this skill decides
how to shape a method's internals once it's placed. When scaffolding a new controller, consumer,
service, or use-case that carries non-trivial logic, load both — they compose, neither overrides
the other.
Concrete limits (defaults — deviate only with a comment explaining why)
| Metric | Target | Hard ceiling | Source |
| Method body lines (NCSS, excl. blanks/comments) | ≤ 30 | 50 | Stricter than Sonar java:S138 default max = 100 (RSPEC-138, jira.sonarsource.com) and Checkstyle MethodLength default max = 150 (MethodLengthCheck.java in checkstyle/checkstyle); aligned with Neal Ford's 15-LoC rule of thumb in The Productive Programmer. |
| Cognitive complexity per method | ≤ 15 | 15 | Sonar java:S3776 default Threshold = 15 — verbatim from CognitiveComplexityMethodCheck.java in SonarSource/sonar-java. Designed by G. Ann Campbell to measure human understandability, not test paths. |
| Cyclomatic complexity per method | ≤ 10 | 10 | Sonar java:S1541 default Threshold = 10; matches McCabe's 1976 IEEE TSE paper "A Complexity Measure" (vol. 2, no. 4, p. 308): "The particular upper bound that has been used for cyclomatic complexity is 10 which seems like a reasonable, but not magical, upper limit." |
| Nesting depth (if/for/try/lambda) | ≤ 3 | 4 | Linux Kernel Coding Style: "if you need more than 3 levels of indentation, you're screwed anyway, and should fix your program." Each nested level also adds +1 in Sonar's cognitive-complexity formula. |
| Parameters per method | ≤ 5 | 7 | Sonar java:S107 default = 7; Checkstyle ParameterNumber default = 7. Above 5, prefer a record/DTO/parameter object (Fowler, Refactoring, Introduce Parameter Object). DI constructors are exempt — use @RequiredArgsConstructor. |
| Class lines | ≤ 300 | 500 | If exceeded it usually fails SRP. |
| File-level public API | One primary public type per file | — | Java convention. |
These are smells, not laws. A coherent 45-line method that reads top-to-bottom is fine; a 25-line method that needs you to chase 6 helpers to understand it is not.
Measure, don't eyeball (on demand — Java)
Cognitive/cyclomatic complexity guessed from reading is unreliable on long methods with nested lambdas, streams, or switches. When complexity is the actual question for a Java method you are about to write, expand, or decompose, get the real number first — then apply the judgment above to it. Do not run this on every edit or as a gate; run it once, at the decision point.
Run the bundled script (it self-locates assets/pmd-ruleset.xml and skips cleanly if PMD is absent):
scripts/measure-complexity.sh <file.java> # or no args → uncommitted + untracked .java filesOutput is one line per method over threshold, e.g.
Foo.java:105: CyclomaticComplexity: The method 'calculate(...)' has a cyclomatic complexity of 13.
The ruleset reports cognitive complexity > 15 (S3776) and cyclomatic > 10 (S1541), matching the table
above. A clean run (no output) means every method is under threshold — no measurement-driven extract
is warranted.
The number informs, it does not command. A high cyclomatic count on a flat switch with no
nesting is often fine to read (that is why the cognitive metric exists); use both numbers as one input
to the "when to extract / when NOT to extract" judgment, never as an automatic trigger. If PMD is not
installed (see compatibility) or the file is Kotlin/TS, skip this step and estimate by reading.
When to EXTRACT a method
Extract when any of these is true:
- A block needs a leading comment to explain what it does — replace the comment with a method whose name IS the comment (Fowler's classic trigger).
- The same (or near-identical) logic appears 2+ times (DRY; rule of three for non-trivial duplication).
- The method mixes levels of abstraction (high-level orchestration interleaved with low-level string parsing) — violates SLAP.
- The method exceeds any threshold above.
- A block has a distinct testable behaviour otherwise tested only indirectly.
- A boolean/conditional takes more than one mental pass to read → extract into
boolean is…()/boolean has…()query method.
When NOT to extract (avoid "tiny methods" antipattern)
Do NOT extract when:
- The extracted method would be called from exactly one place AND its name adds no information beyond the code (e.g.
private int addOne(int x) { return x + 1; }). - Extraction forces a reader to jump to another method to follow a single linear thought (Ousterhout's "shallow module" critique in A Philosophy of Software Design — the cost of the interface exceeds the benefit).
- The extracted code uses 5+ local variables from the caller — the parameter list itself becomes noise. Inline, introduce a parameter object, or refactor the parent first.
- The block is trivial glue (1–3 lines, no branching, no domain concept worth naming).
- Extraction crosses a transactional or reactive boundary in a way that changes semantics — see Stack-specific notes below.
- You're only doing it to satisfy a line-count rule.
Uncle Bob's "functions should be 2–4 lines" stance (Clean Code, ch. 3) is rejected as a default. Martin himself clarified in
johnousterhout/aposd-vs-clean-codeon GitHub: "The 2-4 line functions that you referred to on page 34 were part of the Sparkle applet that Kent Beck and I wrote together in 1999 as an exercise for learning TDD… I claimed no final authority, nor even any absolute 'rightness'. They are offered for consideration."
Step-down rule (Clean Code ch. 4)
Public methods read like a table of contents; private methods sit below them at progressively lower abstraction levels. The first public method of the file should be readable as plain business prose with minimal jargon; details live further down.
Stack-specific notes (Java 25 / Spring Boot 3.x–4.x / WebFlux / PostgreSQL / Redis / SQS)
@Transactional boundaries
Never extract logic out of a @Transactional method into a new @Transactional method on the same class and call it via this.x() expecting a new transaction.
Spring Framework reference docs (Declarative Transaction Management → Using @Transactional): "In proxy mode (which is the default), only external method calls coming in through the proxy are intercepted. This means that self-invocation (in effect, a method within the target object calling another method of the target object) does not lead to an actual transaction at runtime even if the invoked method is marked with @Transactional." The same page notes: "transactional methods in interface-based proxies must always be public" — i.e. a private extracted method cannot carry its own tx.
Fix: extract to a different Spring bean, or switch to AspectJ mode ("Consider using AspectJ mode … if you expect self-invocations to be wrapped with transactions as well").
WebFlux reactive chains
A long Mono/Flux chain is one expression — counting raw lines is misleading. Decompose by extracting each flatMap/map stage that has a distinct business meaning into a private method returning Mono<T> / Flux<T> (e.g. fetchAccount(), validateCreditLimit(), enqueueNotificationEvent()). Do NOT extract one-liner .map(x -> x.toUpper()) stages — that's noise.
Never .block() inside a reactive chain. Never call subscribe() inside a map/flatMap — both lead to leaked subscriptions and uncontrolled execution.
@Service methods
Target ≤ 30 lines, ≤ 3 collaborators per orchestration step. If a service method calls > 5 collaborators, decompose into use-case classes or split the service along the verbs (charge vs. refund, not one giant PaymentService).
Repository methods
Keep them thin; never put business logic in @Query methods or in JdbcTemplate callbacks beyond mapping.
SQS consumers
Separate (1) message parsing/validation, (2) idempotency check (Redis), (3) business action, (4) ack/nack. Each as its own private method; the public @SqsListener reads as a 4-line orchestration.
Examples
❌ Over-decomposed (avoid)
public BigDecimal computeFee(Charge c) {
return addBaseFee(applyDiscount(taxOf(c)));
}
private BigDecimal taxOf(Charge c) { return c.amount().multiply(TAX_RATE); }
private BigDecimal applyDiscount(BigDecimal v) { return v.multiply(NINE_TENTHS); }
private BigDecimal addBaseFee(BigDecimal v) { return v.add(BASE_FEE); }Three single-call helpers add zero clarity over the inline expression and force the reader to scroll through four definitions to verify one formula.
✅ Right-sized
public BigDecimal computeFee(Charge c) {
var tax = c.amount().multiply(TAX_RATE);
var discounted = tax.multiply(NINE_TENTHS);
return discounted.add(BASE_FEE);
}✅ Extraction earns its keep when the name encodes a domain rule
public ChargeResult process(ChargeRequest req) {
rejectIfBlacklisted(req.payer()); // distinct policy
var account = resolveAccount(req.target()); // distinct lookup
var auth = authorizeFunds(req.payer(), req.amount()); // distinct domain step
return submitToLedger(req, account, auth);
}Research-first trigger (Context7 MCP + web search)
Before writing code for a feature, run a Context7 lookup (and web search if needed) when ANY of these apply:
- Introducing a new library, framework feature, or major version (e.g. Spring Boot 3.5 → 4.x, Spring Security 6.x lambda DSL, Reactor 3.7+ APIs, Java 25 scoped values / structured concurrency / pattern matching for
switch). - Using an API you haven't touched in this repo in the last few commits.
- A new architectural pattern in this codebase (saga, outbox, CDC, circuit breaker, idempotency receiver, token bucket).
- Security-sensitive code: auth, crypto, OTP, PII handling, payment-signature verification, secret management.
- Performance-critical path: hot loops, reactive backpressure tuning, PostgreSQL query plans, Redis Lua scripts, SQS visibility-timeout tuning, JVM tuning.
- Cross-service contract change: new SQS event schema, new HTTP contract, breaking API change.
- AWS service integration beyond plain SQS send/receive (DLQ patterns, FIFO + dedup, IAM scoping).
- The user explicitly asks for "best practice", "idiomatic", "latest", "modern", or "current recommended approach".
Procedure:
- State briefly: "Triggering research-first:
<which trigger>." - Call
resolve-library-idthenget-library-docs(Context7 MCP) for the specific topic, and/or a focused web search for blog posts / conf talks from the last 24 months. - Summarise findings in 3–6 bullets, citing the source library/version, before proposing an implementation.
- Then write code.
Do NOT trigger research-first for:
- Trivial CRUD on an existing entity that already follows a clear repo pattern.
- Renames, formatting, dead-code removal, log-message tweaks.
- Bug fixes where the root cause is visible in a stack trace and the fix is localised.
- Tests that mirror an existing test pattern.
- Anything you can implement by copying the shape of nearby code in the same module with confidence.
If unsure, default to a 60-second Context7 check rather than guessing, but never burn a full research cycle on a one-line fix.
Self-check before submitting
Run mentally on each method you wrote/changed:
- One thing, one abstraction level?
- ≤ 30 lines, ≤ 3 nesting levels, ≤ 5 params? (Java: confirm the borderline ones with the PMD measurement above instead of guessing.)
- Name describes intent without needing the body?
- Would inlining it make the caller HARDER to read? If "no," consider inlining.
- Did I introduce a tx/reactive boundary by accident?
Common mistakes
| Mistake | Fix |
| Extracted to satisfy line count, but reader has to chase 6 helpers | Inline back into a coherent 40-line method that reads top-to-bottom. |
Extracted to a new @Transactional private method on the same class | Move to a different bean, or accept the tx will not start. |
| Decomposed a reactive chain by line, not by business stage | Re-extract by domain meaning: fetchX(), validateY(), persistZ(). |
.block() snuck into a flatMap during refactor | Replace with chained Mono.flatMap / Flux.flatMap. |
| Parameter list grew to 6+ during refactor | Introduce a record/DTO parameter object. |
| Refactor silently reordered two I/O calls (HTTP, DB, queue, cache, file, external SDK) | Never reorder calls that cross a process boundary as a side effect of decomposition. First enumerate the ordering invariants — idempotency on retry/replay, state visibility for a later read, audit/happens-before semantics, lock or transaction acquisition order, partial-failure recovery shape — and show each survives. If you can't enumerate them, leave the order as-is. |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

