Think of it like a junior developer
It doesn't have to stay junior. You can enshrine principles into the system instructions (eg AGENTS.md) that will guide it to act more like a senior developer.
Here's what I've got for a general programming baseline:
IV.1.1 PROGRAMMING
The primary technical imperative is managing complexity. Every principle below is a strategy for keeping complexity within the bounds of human cognition. Code that is locally understandable — where a reader can reason about a unit without loading the whole system into working memory — is code that survives contact with reality.
complexity:minimize— Complexity is the root cause of the majority of software defects. Every design choice should be evaluated against: does this make the system harder to reason about? Cleverness that increases local complexity to save lines is a net loss. Simple, obvious code that a tired developer can read at 2 AM is the target.name∈design— A name is a compressed design decision. If a name requires a comment to explain, the name is wrong — rename it until the comment is redundant. Names should reveal intent (remainingRetryCount, notr;fetchActivePatients, notgetData). A misleading name is worse than a bad name — it installs a false model in the reader. Naming difficulty is a design signal: if you can't name it, you don't yet understand what it does, and the fix is to clarify the design, not to pick a vague name and move on.fn:single-task— A function does one thing. The test: can you describe what it does without using "and"? If not, extract. A function that validates and transforms and persists is three functions wearing a trenchcoat. Corollary: a function should operate at one level of abstraction — mixing high-level orchestration with low-level byte manipulation in the same function forces the reader to context-switch continuously.abstract:hide-deep— Good modules are deep: a simple interface hiding significant implementation complexity. A shallow module (complex interface, trivial implementation) pushes complexity outward to every caller. Information hiding is the mechanism: internals that can change without affecting callers are hidden; contracts that callers depend on are explicit. When designing an interface, ask: what does the caller not need to know? Hide that. Leaking implementation details through the interface is the most common abstraction failure.couple:loose— Components should know as little about each other as possible. Tight coupling means a change in one module forces changes in others — the blast radius of every edit expands. Prefer depending on abstractions (interfaces, contracts) over concrete implementations. The Law of Demeter is the heuristic: talk to your immediate collaborators, not their collaborators' collaborators. Tell, don't ask — push behavior to the object that owns the data rather than extracting data and deciding externally.fail:fast-loud— Errors detected late are exponentially more expensive. Validate inputs at system boundaries. Use assertions for conditions that should be impossible — if they fire, the code's model of reality is wrong, and continuing will cause damage. Never swallow exceptions silently; a caught exception with no handling is a lie about error status. Prefer defined error paths over defensivenullreturns that push failure detection to a distant caller. In the same vein: define errors out of existence — where possible, design APIs so that misuse is structurally impossible (type constraints, required parameters, builder patterns) rather than caught at runtime.read>write— Code is read 10–20× more than it is written. Every choice should optimize for the reader, not the writer. This means: consistent formatting, obvious control flow, no surprises. A clever one-liner that saves the writer 30 seconds costs every future reader minutes of decoding. Vertical density matters — but not at the expense of scannability. A blank line between logical sections is not waste; it is a paragraph break for the eye.DRY— Every piece of knowledge has a single, authoritative representation. Duplication is not primarily about lines of code — it is about coupling without a contract. When the same business rule exists in two places, they will inevitably diverge, and the system will contain a silent contradiction. However: mechanical similarity is not semantic duplication. Two functions that happen to look alike but represent different domain concepts should not be merged — they will diverge for good reasons, and a forced abstraction over them creates coupling where none should exist. The test is: if this changes, must that change too? If yes, they are duplicates and should be unified. If no, the similarity is coincidental.compose>inherit— Favor composition over inheritance. Inheritance creates the tightest possible coupling — the subclass is permanently bound to the superclass's implementation. Composition allows assembling behavior from independent, interchangeable parts. Use inheritance for genuine is-a relationships where polymorphism is the goal; use composition for has-a and uses-a relationships. When in doubt, compose. Deep inheritance hierarchies are a code smell — they are rigid, fragile, and force readers to trace behavior across multiple files to answer basic questions.entropy:resist— Software left unattended decays. Every shortcut, every TODO left unaddressed, every "temporary" workaround that becomes permanent adds disorder. The boy scout rule applies: leave code cleaner than you found it. This is not about heroic refactoring — it is about small, continuous improvements: rename a misleading variable while you're in the file, extract a duplicated block when you notice it, delete dead code instead of commenting it out. Broken windows invite more broken windows — the first one matters most.
SUPPORTING PRINCIPLES
- Reversibility. Avoid cementing decisions that are expensive to change. Where the correct choice is unclear, prefer the option that keeps more doors open. Hard-coding a value that might vary is a one-way door; extracting it to configuration is reversible. Committing to an inheritance hierarchy is expensive to undo; depending on an interface is cheap to swap.
- Cohesion. The members of a module should be there for the same reason. A class that handles HTTP parsing, business validation, and database writes has accidental cohesion — the members are together because of workflow sequence, not conceptual unity. High cohesion means every element in a module is essential to the module's single purpose; removing any element would leave a gap.
- Command-query separation. A function should either do something (command — mutate state, produce a side effect) or answer something (query — return a value without side effects). Functions that do both are a source of subtle bugs because callers cannot predict what will happen. Exceptions exist (e.g.,
stack.pop()), but they should be rare and intentional. - Guard clauses over nesting. Prefer early returns for precondition checks over deeply nested
if-elsechains. Each nesting level adds cognitive load; a guard clause eliminates a condition from working memory early. The happy path should be the least-indented path. - Dead code is a liability, not an asset. Commented-out code, unused functions, unreachable branches — delete them. Version control exists. Dead code misleads readers into thinking it matters, increases the surface area for search results, and decays silently as the codebase evolves around it.
- Primitive obsession. When a primitive (
String,int,boolean) carries domain meaning, wrap it. AcustomerIdthat is a rawStringcan be passed anywhere aStringis expected — including where anorderIdis expected. ACustomerIdtype makes that impossible at compile time. The cost is one small class; the payoff is an entire category of bugs eliminated structurally.
CODE DOCUMENTATION
Code documentation lives at the intersection of the code it describes and the reader who will maintain it. Unlike external documentation, it decays alongside its subject — and that proximity is both its strength and its liability.
why>what— Code states what it does. Comments supply what code cannot: intent, constraints, the reason a decision was made. "Retries three times" is in the code. "Retries three times because the upstream service has a known ~2% transient failure rate under load" is documentation. A comment that restates the code is noise (II.6) — delete it or replace it with the why.boundary>interior— Document public interfaces, behavioral contracts, preconditions, and error modes. Internal implementation documentation is warranted only where the non-obviousness is load-bearing for maintainers and cannot be expressed in the code itself. The threshold: could better naming or restructuring make this self-documenting? If yes, restructure first.decay:resist— Documentation coupled tightly to implementation decays with every implementation change. Documentation of contracts decays only when the contract changes — a stronger and more auditable signal. Prefer documenting the stable surface over the volatile interior. Place implementation comments at the exact site of the non-obvious logic, where they are most likely to be updated when the logic changes.precision>completeness— A concise, accurate description outperforms an exhaustive one. If a parameter name is self-explanatory, its Javadoc or docstring must add something the name does not. A description that restates the parameter name is noise. Omit what the reader can derive; document what they cannot.