Skip to content

Latest commit

 

History

History
191 lines (152 loc) · 10.1 KB

File metadata and controls

191 lines (152 loc) · 10.1 KB

My coding standards

My rules for code, every language, every framework. Not style opinions. Break one only with a stated reason. My word rules live in ~/dotfiles/AGENTS.md.

How to decide

When two rules pull apart the order is correct, clear, simple, fast. Fast comes last and only with a measurement. Build for the requirement that exists now, not the one you imagine later. A change is paid once, reading is paid every time, so optimise for reading. Two designs look equal? Pick the one that is easier to delete. Robust and simple fight and you cannot call it? Stop and ask me.

Names

The name says what the thing is or does, in plain words, and a name that lies is the worst defect in this file: fix it before anything else. One concept keeps one name across code, tests, docs, database, API and UI. Short scope takes a short name, long scope takes the full name. No type or pattern noise, so never UserManager, DataHelper, ProcessorImpl or utils. A boolean reads as a claim, is_ready or has_token, never flag or check.

Functions

One job, one level of abstraction; you cannot say it in one sentence, so split it. Return early, because three levels of nesting is a warning and four is a defect. Few arguments, since many arguments mean a type is missing, and no boolean parameter that picks behaviour: write two functions. A function answers a question or changes state, never both, and a plain name hides no write, no call and no global. Do the work with the least computation that is correct.

Modules and boundaries

Build deep modules: a small interface with real work behind it, because a module that only forwards calls earns nothing. The interface hides the decision, so the caller never learns which library, format or order you picked inside. Test the boundary, since a test that breaks on an inside change reached too far in. IMPORTANT one change touches one place; edits forced into files that should not care mean the boundary leaks, and you say so. Depend on the thing that changes least and point the arrows at the stable side.

Layers

The default shape for a service is Domain, Application, Adapters and Infrastructure. Dependencies point inward only: an outer layer knows the inner one, the inner one never knows the outer one.

Domain holds entities, value objects and invariants as plain types, with no framework, no database and no I/O. Application holds the use cases: it drives the domain and declares the ports it needs, and it never implements one. Adapters translate outside to inside, so HTTP handlers, gRPC, CLI and repositories, and they implement the ports Application declared. Infrastructure holds the concrete technology, so pools, clients, file I/O, config and server start, and it wires the layers at the edge.

A domain type carries no serde, ORM or HTTP annotation, because the adapter maps it. A use case takes ports and not clients, so you test it with fakes and no database. A business rule inside a handler or a SQL query is a defect: move it in. Pure logic stays apart from I/O, and that is what the inward arrow buys. A script or one small binary does not need four layers, two is enough.

Patterns

Gang of Four, 1994. A pattern names a shape that already fits, never a target to build toward. Reach for one when the problem is that pattern's problem, else write plain code.

Creational patterns decide who makes the object. A factory method picks the concrete type at runtime so the caller never names it. An abstract factory keeps a family of types matched, such as one driver set per backend. A builder handles many optional fields and makes a half-built value unusable. A prototype copies when a copy costs less than a build. A singleton is the last resort, because it is hidden global state; pass one instance from the composition root instead.

Structural patterns decide how the parts fit. An adapter bends a third-party interface to your port and is the main tool of the adapter layer. A facade puts one small entry over a big subsystem, the same idea as a deep module. A decorator adds retry, cache, log or metric without touching the wrapped type, and a proxy controls access by lazy load, remote call or permission check. A composite makes a tree of parts that the caller handles as one part. A bridge fits when abstraction and implementation both vary, which is rare, so prove both axes move. A flyweight fits many near-equal objects when memory hurts, so measure first.

Behavioural patterns decide how the parts talk. Strategy swaps an algorithm at runtime and kills a chain of branches, while state changes behaviour with the state and replaces a state flag plus branches. Observer is publish and subscribe, one event and many readers. Command turns an action into a value you can queue, retry, undo or audit. Iterator walks a collection without showing what is inside. A template method fixes the skeleton and varies the steps, but try strategy first. Chain of responsibility is middleware, where each link handles the call or passes it, and a mediator gives many-to-many talk one hub, which you watch, because a hub grows into a god object. Memento takes a snapshot for undo. Visitor fits a stable data shape with a growing set of operations and costs a lot when the shape moves. Interpreter fits when you own a small language and is rarely worth the weight.

Name the type after the job and never the pattern: RetryingPaymentClient, not PaymentDecorator. One pattern per problem, because two stacked patterns hide the problem, and a pattern that adds an indirection but no new choice is dead weight you delete.

Data and state

Shape state by use, so cached state stays light next to state that goes to disk. Validate untrusted input once, at the boundary, into a trusted type, and inside is then trusted. Prefer values that do not change, and change them in one place when you must. Keep one source of truth per fact, because a second copy goes stale. No hidden global state: pass what the code needs.

Errors

A programmer error, a broken invariant or an impossible state fails fast and loud, and goes up to the user. An expected runtime failure such as network, disk or user input gets handled with a message that says what failed and what to do. Never swallow an error: no empty catch, no log-and-continue that pretends it worked. Never use null, none or an empty value to mean "not found", use an explicit type. An error message names what failed and the value that caused it, and never a secret, a token or personal data. My rule: errors are first-class values, acknowledged and handled at compile time, the Rust way, where it applies and matters.

Consistency

Match the file you are in, because its naming, layout, error style and test style beat habit and beat the examples here. One way per project: it already has a logger, an HTTP client, a date helper or a result type, so use it and never add a second. Need a new pattern? Ask, and never leave two patterns side by side. Never reformat or rename code you did not have to touch, it hides the real diff.

Smells to fix on sight

Fix these in the code you touch, do not hunt the whole repo. The same logic twice, pulled out on the second copy and not the third. A function or file so long it needs comments as section markers. A magic number or string with no name. Dead code, an unused parameter, an unreachable branch, and commented-out code, all deleted, because git remembers. A comment that repeats the code goes, a comment that holds a decision stays. A TODO with no owner and no date gets done, filed or dropped. No broken windows, small decay invites more.

Tests

Tests come first, then the feature, never the other way. Keep the count minimal and test where there is a real need. Test behaviour and not implementation, so a refactor breaks a test only when the behaviour changed, and a bug fix starts with a test that fails for that bug. The test name says the case and the expected result. Test the edges: empty, one, many, wrong type, too big and the failure path. A flaky test is worse than no test, so fix it or delete it. No test touches the network or the clock, you control both. Tests stay fast, deterministic and apart from each other. Coverage is a hint, not a goal; never write a test to move a number.

Security

No hardcoded secret, key or credential, not in examples and not in tests. Never log personal data, tokens or passwords, at any level. Untrusted input is data, never code and never a query, so parameterise it. Default deny, and grant the smallest permission that works. Never invent your own crypto or auth, use the standard library or a known one.

Dependencies and config

Add a dependency only when it earns its weight, because a few lines of your own often win, and check size, transitive deps, maintenance and licence before you add it. Pin versions, commit the lockfile, and delete a dependency you stopped using. Config comes from the environment so one build runs in dev and prod, and secrets live outside the repo, always. Keep the project manifest clean and current.

Version control

One logical change per commit, never a dump of unrelated edits, and the commit builds and passes its tests on its own. History reads as a timeline and rebuilds the project. Never commit a secret or build output, they go in gitignore on day one. Commit message rules live in ~/dotfiles/AGENTS.md.

Documentation (follow ~/dotfiles/VOICE.md)

Compact and rich, never a wall of text. A doc-string says what the thing does, its inputs, its outputs and what it raises, in short Google style. A comment carries the why, so the decision, the trade-off or the odd line, and only where a reader would guess wrong. A README says what it is, how to run it and how to test it, nothing else. Documentation that is wrong is worse than none, so fix it in the same commit.

Done means done

It runs, the tests pass, and the linter and type checker are clean, because you ran them. New behaviour has a test and the fixed bug has a test. No debug print, no scratch file, no leftover dead branch. You read the whole diff, and anything you cannot defend comes out. Avoid at all cost a step failed or you skipped it? Say so plainly, and never report done over a failing test.