OTAKU / The 40 rules

Track O — Own your Discipline

The forty rules

Forty rules, each with a threshold. They are mechanical for a reason: a rule that depends on taste cannot be reviewed, automated or argued with.

Read the rule, then check whether your code passes. When in doubt, choose the more restrictive option — that is the tie-breaker the philosophy prescribes.

The primary metric

Cyclomatic complexity ≤ 5 per method. This is the main instrument, not a decoration. It counts the independent paths a reader must hold in their head at once: every if, every loop, every boolean operator adds one.

A method at 6 is rejected, even if it is short and beautifully indented. The other rules exist to keep this number low — guard clauses remove branches, extraction moves them into names, polymorphism replaces switch.

All forty

OComplexity

  1. Cyclomatic complexity per method must not exceed 5.
  2. Only one level of indentation per method.
  3. Do not use the ELSE keyword (guard clauses or early returns).
  4. Never use switch/case (polymorphism, match expressions or strategy).
  5. Avoid magic numbers and strings (constants or enums).

OSize

  1. Maximum of 50 lines per class (code inside the class body only).
  2. Maximum of 20 lines per method (executable code only).
  3. Maximum of 10 classes per namespace or package.
  4. No class with more than two instance variables.
  5. Methods must have no more than two parameters.
  6. Constructors must have no more than two parameters.
  7. Interfaces must have no more than two methods.

ONaming

  1. Do not abbreviate; use full, descriptive names.
  2. Boolean methods start with is, has, can or should.
  3. Method names start with a verb.
  4. Constants are UPPER_SNAKE_CASE.
  5. Property and method names are camelCase.

TModularity

  1. First-class collections: wrap arrays in dedicated classes.
  2. One dot per line; avoid method chaining for readability.
  3. Static methods are forbidden except factories and pure functions.
  4. Commands and queries are separate: no side effects in value-returning methods.
  5. Functions are pure when possible: same input, same output, no side effects.
  6. No global state or singletons; inject dependencies explicitly.
  7. Prefer small, focused functions; extract when a method does more than one thing.

AProtection

  1. No getters, setters or public properties; use behaviour methods.
  2. Properties are private by default; protected only for framework inheritance.
  3. Classes are final by default; extension happens through interfaces.
  4. Inheritance is forbidden except framework base classes; prefer composition.
  5. Validate all input at the boundary: constructors and public methods.
  6. Fail fast: throw as soon as a precondition is violated.
  7. Never mutate input parameters; treat them as immutable.

KSide effects and failure

  1. Prefer immutability: readonly properties, return new instances.
  2. Methods returning collections return empty collections, never null.
  3. Methods that may fail return Result/Either or throw; never null.
  4. Explicit over implicit: no hidden dependencies, magic methods or implicit conversions.
  5. Never leak infrastructure details (SQL, HTTP, file paths) into the domain.

UContracts

  1. Wrap all primitives and strings in value objects.
  2. No generic types (mixed, array, object) except explicit serialization methods.
  3. Every public method is declared in an interface.
  4. Comments explain why, and belong in contracts, not implementations.

The rule that is most often misread

Rule 25 forbids getters, setters and public properties. The usual mistake is to delete the getter and make the field public — which is worse, because now the invariant has no guardian at all.

The point is not visibility. It is ownership: the object must be the one that operates on its own attributes. Asking for state and changing it outside is procedural programming with classes.

final class Account
{
    public int $balance = 0;
}

$account->balance = $account->balance - 500;
final class Account
{
    private function __construct(private Money $balance) {}

    public function withdraw(Money $amount): self
    {
        if ($this->balance->isLessThan($amount)) {
            throw new InsufficientFunds($amount);
        }

        return new self($this->balance->subtract($amount));
    }
}

$account = $account->withdraw(Money::fromCents(500));

Both wrong examples below are equivalent: one exposes the field, the other exposes the field through methods. Neither protects the balance.

public sealed class Account
{
    public decimal Balance { get; set; }
}

account.Balance -= 500m;
public final class Account {
    private long balance;

    public long getBalance() { return balance; }
    public void setBalance(long balance) { this.balance = balance; }
}

account.setBalance(account.getBalance() - 500);
export class Account {
  public balance = 0;
}

account.balance -= 500;
export class Account {
  private constructor(private readonly balance: Money) {}

  withdraw(amount: Money): Account {
    if (this.balance.isLessThan(amount)) {
      throw new InsufficientFunds(amount);
    }

    return new Account(this.balance.subtract(amount));
  }
}

The rule that is most often ignored

Rule 9 — at most two instance variables. Three fields usually mean three responsibilities sharing a constructor, or one unnamed concept.

// Three instance variables: the concept has no name yet.
final class Service
{
    public function __construct(
        private ServiceName $name,
        private ServiceHost $host,
        private ServicePort $port,
    ) {}
}

// The concept is an endpoint. Name it, and the class drops to two fields.
final class Service
{
    private function __construct(
        private ServiceName $name,
        private Endpoint $endpoint,
    ) {}
}

The second version is not shorter by accident. Endpoint is the name the codebase was missing, and it is now the place where host and port validate each other.

Rules that change a signature

RuleWhat it removesWhat it adds
10, 11 — two parameterslong constructors and methodsparameter objects with names
12 — two methods per interfacebroad “service” interfacesrole interfaces, one per client
34 — no nullnull as an undocumented third stateResult/Either or a domain exception
39 — every public method in an interfaceconcrete couplinga contract you can fake in tests
// Failures are values, not nulls.
final class WithdrawMoney
{
    public function __invoke(AccountId $id, Money $amount): Result
    {
        $account = $this->accounts->find($id);

        return $account->canWithdraw($amount)
            ? Result::success($account->withdraw($amount))
            : Result::failure(new InsufficientFunds($amount));
    }
}

Measuring the primary metric

LanguageToolSignal
PHPPHP_CodeSniffer (CyclomaticComplexity), PHPStan, phpMetricsfail above 5
C#Roslyn analyzers (CA1502), NDependwarn/fail above 5
JavaPMD (CyclomaticComplexity), Checkstyle, SonarQubefail above 5
C++clang-tidy readability-function-cognitive-complexity, lizardfail above 5
TypeScriptESLint complexity, ts-archfail above 5

Set the threshold in the pipeline, not in a wiki. A metric that is only visible in review is already dead.

Applying this to an existing codebase

  1. Turn on the complexity check in report mode and count violations.
  2. Pick the worst file, extract until the top method is below 5.
  3. Lock that file with a baseline: it may not get worse.
  4. Repeat weekly. The baseline shrinks or the number of violations freezes — both are progress.