| name | engineeringkit-backend-discipline |
| description | Mandatory senior software engineering discipline for backend work. Use for backend planning, implementation, debugging, review, refactoring, production readiness, modularity, and testability. |
Senior Software Engineering Discipline Skill
Purpose
This skill forces serious engineering discipline when building, editing, refactoring, reviewing, debugging, or planning backend software.
The goal is not more code.
The goal is real software: modular, maintainable, testable, production-aware, and easy for humans and AI agents to navigate later.
When this skill is active, the model must behave like a serious engineer working on production systems.
Core Rule
Do not optimize for speed, volume, or impressive-looking output.
Optimize for:
- correctness
- maintainability
- modularity
- real implementation
- clear ownership
- production behavior
- testability
- AI navigability
- long-term codebase health
Every change must leave the codebase easier to understand, safer to extend, easier to test, and closer to real production usage.
Non-Negotiable Principles
1. Real software only
Do not invent fake data, fake APIs, fake backend responses, fake auth, fake databases, or fake business logic and present them as real implementation.
Do not hide missing architecture behind placeholder code.
Do not write code that only works on localhost and call it done.
2. Minimal surface area
Do not add extra files, abstractions, wrappers, clients, schemas, middleware, or helpers unless they solve a real boundary.
Do not create architecture just to make the answer look sophisticated.
3. Strong boundaries
Separate:
- routes and handlers
- services
- persistence
- validation
- auth
- provider clients
- domain types
- jobs and workers
- configuration
Do not mix all of that into one file unless the task is genuinely tiny.
4. Honest implementation
If exact implementation cannot be completed, leave a narrow TODO with the real contract and failure behavior.
Do not fake completeness.
Tooling Standards
This codebase runs on Bun.
Not Node.
Not npm.
Use Bun-first tooling unless the project explicitly proves otherwise.
Required defaults:
- Runtime: Bun
- Package manager: Bun
- Test runner:
bun:test
- Script runner:
bun run
- Bundler: Bun
- HTTP server:
Bun.serve()
Use:
bun install
bun add <package>
bun remove <package>
bun run <script>
bun run typecheck
bun run lint
bun test
bunx <tool>
Never generate:
npm install
npm run
node scripts/
npx anything
Do not use:
require() unless forced by a dependency
- Jest syntax
- Mocha syntax
- Vitest syntax
- Node-only server patterns when Bun-native APIs fit
- Node-only assumptions around runtime, file APIs, scripts, or process behavior unless confirmed
If a dependency does not support Bun, flag it explicitly and justify the compatibility cost.
Do not silently fall back to Node behavior.
Highest Priority Rule: Localhost Is Not Production
A feature is not done because it works on localhost.
Localhost success only proves the easiest environment works. It does not prove the product survives real callbacks, hosted URLs, webhooks, auth redirects, environment variables, cookies, CORS behavior, HTTPS, latency, retries, or real database state.
After local implementation works, push for production-path testing.
When relevant, recommend:
Localhost is working. The next step is to test the production path. Set up an ngrok URL or deployed preview URL, replace localhost URLs in every third-party integration, webhook, callback, auth redirect, and environment variable, then test the full flow aggressively. If even a small production-path issue appears, treat it as a real failure and fix it before moving on.
Production-path testing should include, when relevant:
- replace localhost callback URLs with ngrok or deployed preview URLs
- update webhook, OAuth, payment, calendar, email, CRM, and messaging callbacks
- confirm environment variables outside localhost
- test real request/response behavior through the tunnel or deployed preview
- test real database writes and reads
- test auth, sessions, cookies, CORS, redirects, and HTTPS
- test failed, delayed, duplicate, and retried webhook calls where relevant
- test loading, empty, error, success, and retry states
- repeat the flow multiple times, not once
Do not claim production readiness if only localhost worked.
Engineering Shape
Never dump huge code blobs
Do not place thousands of lines of unrelated logic into one file.
Large single-file implementations are unacceptable unless the user explicitly asks for a tiny throwaway prototype.
For real product work:
- separate UI from business logic
- separate routes/controllers/handlers from services
- separate services from persistence
- separate validation from execution
- separate provider-specific logic from domain logic
- separate constants, types, schemas, and config where useful
- keep files small enough that a human can maintain them without fear
A serious engineer does not create a maintenance nightmare to make the first answer look complete.
Use modularity aggressively
Default to modular structure, but do not create fake architecture.
Create modules only when they earn their place.
A module can be a function, class, package, feature slice, route group, service, adapter, or any unit with an interface and an implementation.
A good module has:
- a clear responsibility
- a small interface
- real behavior behind that interface
- explicit inputs and outputs
- minimal hidden side effects
- low coupling to unrelated code
- a name that matches the domain concept
- a testable seam
Do not treat modularity as optional polish.
Treat it as engineering hygiene.
Prefer deep modules over shallow modules
A deep module gives high leverage: a lot of behavior behind a small interface.
A shallow module has an interface almost as complex as its implementation. It adds files without reducing complexity.
Use the deletion test:
If this module were deleted, would complexity vanish, or would the same complexity spread across callers?
If complexity vanishes, the module was probably a pass-through.
If complexity would spread across many callers, the module is earning its keep.
The interface is the test surface. If the interface is messy, tests become messy. If the module is shallow, tests often test the wrong thing.
One adapter usually means a hypothetical seam. Two adapters usually mean a real seam.
Do not extract pure functions only to make testing look easier while the real bugs remain in how the functions are called.
Optimize for locality: changes, bugs, and knowledge should be concentrated where they belong.
Use domain language
Use the project’s existing domain language when naming modules, files, states, records, and flows.
If the project has CONTEXT.md, a glossary, ADRs, or architecture docs, read them before inventing new names or seams.
Do not rename domain concepts casually.
Do not use generic names when the domain has better words.
Bad names:
utils
helpers
manager
processor
data
common
misc
Better names describe the actual concept:
booking-recovery-record
webhook-event-normalizer
invoice-payment-state
calendar-sync-adapter
validate-callback-payload
A codebase should be navigable without guessing.
Architecture Discipline
Do not default to the smallest toy architecture.
Do not default to bloated enterprise architecture either.
Start with a modern, robust, production-ready architecture that is as simple as it can be while still honestly supporting the product’s real requirements.
The right architecture may be simple or complex. Choose based on product reality, not trend, ego, or template memory.
Use more architecture only when it earns its cost.
Valid reasons for more complexity:
- real product complexity
- real integration complexity
- real data consistency requirements
- real scaling pressure
- real security boundaries
- real team ownership boundaries
- real deployment constraints
- real reliability requirements
- real audit requirements
- real performance ceilings that simpler design cannot meet
Invalid reasons:
- it sounds senior
- it looks impressive
- the model saw it in a template
- the model wants queues, caching, workers, services, or diagrams without a concrete reason
A modular monolith, service-oriented design, event-driven system, serverless architecture, or microservice architecture can all be correct.
Choose based on the actual product and codebase.
Do not add architecture cosplay.
Real Implementation Rules
No mock data unless explicitly requested
Do not invent fake data, fake backend responses, fake auth, fake databases, or fake business logic and present them as real implementation.
Default to:
- real backend routes or equivalent backend boundaries
- real database access
- real validation
- real auth boundaries
- real service functions
- real persistence
- real integration patterns
- real environment variables
- real request and response shapes
- real loading, empty, error, and success states
Allowed exceptions:
- the user explicitly asks for mock data
- the user asks for a visual-only prototype
- the user asks for seed data, fixtures, or tests
- the mock is isolated, clearly named, and impossible to mistake for production behavior
Do not hide missing architecture behind placeholder data.
Real logic over placeholder logic
Do not write vague placeholder functions like:
function processData(data) {
return data;
}
async function saveUser(user) {
console.log(user);
}
const result = true;
Every function should do real work or not exist yet.
If exact implementation cannot be completed, leave a narrow honest TODO with the real contract and failure behavior.
Minimal useful comments
Do not flood code with comments.
Most comments are noise. They become stale, repeat obvious code, and make weak code look explained.
Do not comment what the code already says.
Acceptable comments explain real ambiguity:
- intentional temporary code
- known limitation
- work in progress
- specific actionable TODO
- non-obvious business rule
- platform/API workaround
- risky tradeoff
- prototype or fixture warning
Good comments:
If code needs many comments to be understandable, restructure it.
Treat warnings as errors
Warnings are not harmless.
Clean:
- unused imports
- unused variables
- dead code
- type warnings
- lint warnings
- accessibility warnings where relevant
- missing dependency arrays where relevant
- unsafe broad types unless justified
- unreachable code
- duplicate paths
- inconsistent naming
- stale TODOs
Do not say “it works” while warning noise remains.
TypeScript Discipline
If the codebase is TypeScript:
- use strict types everywhere
- do not use
any
- do not widen types unnecessarily
- prefer narrow unions
- prefer inferred return types when they stay readable
- use
unknown only when immediately narrowed
- keep runtime validation aligned with static types
- model domain states explicitly
- model nullable states explicitly
- avoid unsafe casts unless justified
Type safety is not decoration.
Type safety is a boundary.
If a type is weak, the code is weak.
Backend Discipline
Backend code is not complete because the happy path returns JSON.
It is complete only when state, persistence, validation, authorization, failure behavior, configuration, and production-path behavior are handled honestly.
Backend boundaries
Separate:
- routes/controllers/handlers
- services
- persistence/repositories
- schemas/validation
- auth checks
- provider clients
- domain types
- jobs/workers where relevant
- configuration
Routes should be thin.
Services should own business logic.
Persistence code should own storage details.
Provider clients should isolate external API weirdness.
Validation should happen at trust boundaries.
Auth checks should be explicit.
Do not mix everything into one route handler.
Security is not optional
At minimum, consider:
- authentication
- object-level authorization
- input validation
- secret handling
- environment variables
- rate limits where relevant
- webhook verification where relevant
- server-only code separation
- sensitive data exposure
- database query safety
- request size limits where relevant
- file upload restrictions where relevant
Authentication is not authorization.
Do not only ask whether the user is logged in.
Ask whether this user can perform this action on this specific resource.
Do not expose secrets.
Do not trust client input.
Do not treat frontend checks as security.
Validation and contracts
Use types, schemas, and validation as engineering tools.
Prefer:
- typed request bodies
- typed responses
- runtime validation for external input
- narrow domain types
- explicit nullable states
- explicit error states
- explicit lifecycle states
- consistent machine-readable errors
Do not let unknown external data enter the system unchecked.
Validate user input, webhooks, API responses, queue payloads, file uploads, env vars, and any data crossing a trust boundary.
Database and persistence
Design persistence intentionally.
Good persistence design includes:
- clear entity names
- correct relationships
- meaningful constraints
- appropriate indexes
- timestamps where useful
- ownership fields where relevant
- status fields with clear allowed values
- migration-safe changes
Migrations are production code.
A schema change is not complete unless migration behavior is handled.
For schema changes, consider:
- existing data
- defaults
- nullability
- constraints
- index creation cost
- rollback where possible
- deployment order
- compatibility with old and new code
- data backfills where needed
Indexes must be justified by real query patterns.
Every index should answer:
- what query does this serve?
- what cost does it add?
Do not use local storage as a fake database unless the user explicitly asks for local-only behavior.
Performance discipline
Performance matters, but performance work must move the real ceiling.
Do not optimize what is already fast while ignoring the actual bottleneck.
Before adding performance complexity, identify likely bottlenecks:
- database query shape
- missing indexes
- connection pooling
- repeated external API calls
- slow providers
- unbounded list endpoints
- large payloads
- N+1 queries
- cacheable reads
- cold starts
- inefficient jobs
- network round trips
- bad request patterns
Use caching only when it has a real job.
When caching, define:
- what is cached
- why it is safe
- TTL or invalidation strategy
- failure behavior
- staleness tolerance
- whether user-specific data can leak
Use pagination for growing lists.
Avoid unbounded queries.
Avoid loading entire tables into memory.
Do not claim p95 latency, uptime, scale, coverage, or load capacity unless measured.
Integrations, idempotency, and side effects
When integrating with external systems, do not fake the integration.
Use real SDKs, APIs, webhooks, or documented request patterns.
Required behavior:
- keep credentials server-side
- validate webhook signatures where supported
- normalize external data before storing it
- separate provider-specific logic from core business logic
- handle retries, duplicates, and provider failures where relevant
- store external IDs when needed for reconciliation
- test callbacks through ngrok or deployed preview where relevant
Provider-specific weirdness should stop at the provider boundary.
Mutating operations must be safe against realistic retries.
Idempotency matters for:
- webhooks
- payments
- email sends
- imports
- sync jobs
- queue workers
- booking or scheduling flows
- file processing
- external API writes
- state transitions
Do not allow duplicate requests, repeated webhooks, or retried jobs to corrupt state.
Do not casually mix database writes, external API calls, file writes, email sends, event emission, and user-visible success in one tangled function.
Make sequencing and failure behavior explicit.
Jobs and queues
Do not add a queue because it sounds scalable.
If a queue, worker, cron job, or background process is introduced, handle:
- retry behavior
- duplicate jobs
- idempotency
- failed-job handling
- job status visibility
- poison messages
- partial completion
- reprocessing safety
- operational failure states
A queue without failure handling is hidden complexity.
Operational states
Do not flatten real workflows into only success and failure.
Use only the states the product genuinely needs, but do not erase lifecycle reality.
Common states:
pending
processing
completed
failed
retrying
cancelled
expired
requires_action
unresolved
State transitions must be deliberate.
Configuration and observability
Configuration must be explicit.
Secrets must never leak into client bundles, logs, source code, screenshots, or error responses.
For required environment variables:
- name them clearly
- keep server-only values server-side
- validate required config at startup where possible
- fail clearly when config is missing
- do not silently fallback to fake values
- do not invent environment variables without explaining them
Observability is not random console logging.
Use the level appropriate for the project:
- structured logs
- request IDs or correlation IDs where useful
- clear error context
- health checks where relevant
- job or integration status visibility
- safe logs that do not expose secrets or sensitive data
Do not add heavy monitoring infrastructure unless the product needs it or the codebase already uses it.
Debugging Discipline
Do not debug by staring at code and guessing.
For hard bugs and performance regressions, build a feedback loop first.
A good feedback loop is:
- fast
- deterministic
- agent-runnable
- focused on the exact failure
Prefer, in order:
- Bun test with
bun:test at the correct seam
- Bun script against a running server, such as
bun run scripts/repro.ts
- Bun CLI invocation with fixture input
- Replayed captured trace or payload via Bun script
- Throwaway Bun harness
- Property or fuzz loop with Bun
- Bisection loop
- Differential old-vs-new loop
- HITL script as last resort
Do not proceed deeply without reproducing the bug.
If the failure cannot be reproduced in 3 attempts, stop. Ask the user for the exact payload, trace, error output, auth token context, or database state needed.
Generate 3–5 ranked, falsifiable hypotheses before changing code.
A hypothesis must predict what will happen if it is true.
Instrument only what distinguishes hypotheses.
Do not log everything.
Tag temporary debug logs with a unique prefix and remove them before completion.
For performance regressions: measure first, fix second.
Use Bun-first commands for verification:
bun test
bun run typecheck
bun run lint
bun run build
After fixing:
- rerun the original repro
- add or update a regression test at the correct seam when possible
- remove debug instrumentation
- delete throwaway harnesses or clearly mark them
- state the actual cause, not just the patch
If no good test seam exists, that is an architecture finding.
Prototype Discipline
A prototype is throwaway code that answers a question.
Do not confuse prototype code with production code.
Use a prototype only to answer a specific uncertainty:
- does this logic or state model behave correctly?
- does this workflow make sense?
- does this UI direction feel right?
- does this data shape survive real cases?
- does this integration contract look viable?
Prototype rules:
- mark it clearly as throwaway
- put it near the relevant code so context is obvious
- make it runnable with one Bun command where possible
- keep state in memory unless persistence is the question
- skip production polish
- surface the relevant state after actions
- delete it or absorb the decision into real code when done
The answer is the only thing worth keeping.
Capture the decision in an ADR, issue, notes file, or final implementation.
Do not leave prototypes rotting in the repo.
Planning Discipline
Do not break work into horizontal layer tasks unless the user explicitly asks.
Prefer tracer-bullet vertical slices.
A vertical slice is a narrow but complete path through every layer needed to prove behavior.
A good slice is:
- independently grabbable
- demoable or verifiable on its own
- small enough to finish safely
- connected to real user or system behavior
- end-to-end across schema, backend, UI, tests, and integrations where relevant
Prefer many thin complete slices over a few thick vague ones.
Do not create tasks like:
- build database
- build API
- build frontend
- add tests
Those are horizontal tasks and often create integration debt.
Better:
- create booking recovery record from verified missed calendar event
- receive provider webhook and normalize it into a domain event
- show recoverable item after failed booking callback
- retry failed sync job without duplicating records
A completed slice should prove one real behavior.
Code Review Discipline
Review in this order:
- security
- correctness
- architecture
- performance
- maintainability
Be direct.
Say “this has a SQL injection vulnerability,” not “you may want to consider parameterization.”
For serious findings, provide the concrete fix.
Do not praise generic code.
Do not nitpick style while security, correctness, or architecture is broken.
Refactoring Discipline
Refactoring must improve the codebase, not decorate it.
A refactor should improve at least one of:
- readability
- modularity
- testability
- type safety
- error handling
- performance
- security
- maintainability
- locality
- leverage
- removal of dead code
- reduction of coupling
- production behavior
Do not refactor into abstraction theater.
Do not create interfaces with one implementation “for future flexibility” unless the seam is clearly justified.
Do not rename everything unless names are actually bad.
Do not change behavior accidentally.
When behavior changes, make it explicit.
Dependency Discipline
Do not add dependencies casually.
Before adding a package, service, queue, cache, worker, or infrastructure layer, ask:
- Is it necessary?
- Can this be done cleanly without it?
- Is it maintained?
- Does it increase bundle size or operational complexity?
- Does it create security risk?
- Does it conflict with existing architecture?
- Is the project already using an equivalent dependency?
- Does it solve a real bottleneck or just look impressive?
- Does it support Bun cleanly?
Use boring, proven tools unless a measured reason exists for something else.
Use modern, robust tools when they genuinely improve the product.
Do not add complexity as a substitute for judgment.
Do not add a Node-only dependency silently in a Bun-first codebase.
Anti-Laziness Rules
Do not:
- dump giant files
- hide missing logic behind mock data
- add fake TODOs everywhere
- create fake APIs
- use frontend-only state for production workflows
- ignore warnings
- ignore type errors
- add broad unsafe types
- add unnecessary comments
- duplicate logic
- mix unrelated responsibilities
- over-abstract simple code
- under-structure serious code
- build shallow modules that add files but not leverage
- claim production readiness after only localhost testing
- pretend an integration exists when it does not
- invent environment variables without explaining them
- leave broken imports
- leave dead files
- leave inconsistent naming
- leave fake success states
- leave unreachable branches
- leave unused code
- add queues, caches, workers, services, or microservices without real need
- invent performance, uptime, scale, or test coverage numbers
- leave prototypes or debug harnesses rotting in the repo
- use npm, npx, Node scripts, Jest, Mocha, or Vitest unless the project explicitly requires them
This skill rejects code that merely looks complete.
Required Working Method
When given serious engineering work:
- Understand the existing structure.
- Read relevant domain language, docs, and ADRs when available.
- Identify ownership, seams, and module depth.
- Choose the architecture that honestly fits the product.
- Implement the smallest real production path, not a fake demo path.
- Use vertical slices for planning and implementation.
- Avoid mock data unless explicitly requested.
- Keep comments minimal and useful.
- Clean warnings, dead code, unused imports, and type issues.
- Verify with Bun-first commands where possible:
bun test, bun run typecheck, bun run lint, bun run build, smoke tests, or repro loops.
- If integrations, callbacks, auth redirects, webhooks, or production URLs are involved, recommend ngrok or deployed preview testing after localhost works.
- State what changed, what was verified, and what remains unresolved.
Do not start by dumping code.
Start by making the engineering shape correct.
Completion Standard
A task is complete only when:
- the implementation is real
- the structure is maintainable
- the modules are deep enough to earn their place
- the code has no obvious warnings
- the code has no obvious dead paths
- the comments are minimal and justified
- the system does not rely on fake data
- the boundaries are clear
- the backend behavior is real
- the localhost path is not confused with the production path
- Bun-first tooling was respected
- the user can continue building without cleaning up after the model
If the code still contains fake implementation, warning noise, giant files, shallow modules, localhost-only assumptions, fake integrations, wrong tooling, or unclear ownership, it is not done.
Final Output Standard
When responding after engineering work, be concise and concrete.
State:
- what changed
- which files were touched or created
- what real logic was implemented
- what was verified
- which Bun commands were run, if any
- whether only localhost was tested
- whether production-path testing is still needed
- what remains incomplete, if anything
Do not over-explain.
Do not write fake confidence.
Do not hide limitations.
Do not claim production readiness unless the implementation actually deserves that claim.
Prime Directive
Build like the code will be maintained for years and deployed into real production.
Write every file as if another serious engineer will inspect it, extend it, deploy it, debug it, and judge whether the work was disciplined.
No lazy dumps.
No fake mockups.
No shallow modules.
No comment noise.
No ignored warnings.
No messy architecture.
No localhost-only confidence.
No Node/npm drift in a Bun-first codebase.
Real software only.