Skip to main content
Every non-trivial architecture decision has been made once, documented here, and is now the law. This catalogue exists so the same question is never debated twice, and so a reviewer has a canonical reference when flagging a violation. Consistent patterns reduce cognitive overhead: once you know how GameSession is wired, you know how every session-like class is wired.
Any banned pattern introduced into the codebase must be explicitly justified in the PR description and approved by the technical lead. There are no quick exceptions, no “just this once.”
The diagram below shows how the adopted patterns interact at runtime. Dependencies flow inward — outer layers depend on inner layers via interfaces, never the reverse.

Adopted Patterns


Pattern Detail: Clean Architecture Layering

The project is split into four concentric layers. Dependencies flow inward only — outer layers know about inner layers, never the reverse. Why this matters in practice: SetValidator has no using UnityEngine;. This means you can run the entire game loop in a unit test in milliseconds with no Unity process. It also means the domain logic can be ported cleanly to the Nakama server side as the authoritative reference. Review checklist:
  • No using UnityEngine; in SET.Domain or SET.Application
  • No using Nakama; in SET.Domain or SET.Application
  • Infrastructure classes implement interfaces declared in Application or Domain
  • SET.Presentation does not import SET.Infrastructure directly — only through interfaces

Pattern Detail: Constructor Injection (VContainer)

All dependencies are declared as constructor parameters and injected by VContainer at startup. No class creates its own dependencies with new ConcreteClass() (except in factories, which exist precisely to own that responsibility).
Review checklist:
  • No new ConcreteClass() inside Application or Domain methods
  • All injected fields are interface types, not concrete types
  • All DI bindings are in the Bootstrap scene — no scattered registration

Pattern Detail: Command / Query Separation (CQS)

User input and AI decisions become commands — immutable value objects sent to GameSession.HandleCommand(). They have no return value. State is read separately as queriesGameSession exposes an IObservable<GameStateSnapshot> that pushes a new immutable snapshot after every state change.
Review checklist:
  • Commands are immutable records — no mutable properties
  • No command type has a return value (use IObservable<MatchEvent> for async feedback)
  • Queries return read-only data — no mutable domain objects leak through a snapshot

Pattern Detail: Finite State Machine (GameSession)

GameSession uses an explicit MatchState enum and a guarded HandleCommand switch. Any command received in the wrong state is silently discarded — no exceptions, no corrupted state.
If you find yourself adding an else branch that does something when the state is wrong, stop. The FSM is supposed to silently discard invalid inputs. If a bad state is reached, log a warning — don’t try to recover ad hoc.
Review checklist:
  • Every valid transition is explicitly listed in the switch
  • Input is blocked (discarded) during animation lock states
  • AnySetExists() is called after every board mutation
  • End-game condition is checked after every state transition

Pattern Detail: Adapter (Wrapping External SDKs)

Nakama, Google Play Games Services, and any other third-party SDK lives entirely inside SET.Infrastructure. The Application layer declares the interface; the Infrastructure layer implements it.
Review checklist:
  • No Nakama types (IMatch, IMatchState, ISocket) appear in Application or Domain assemblies
  • Adapter methods are thin wrappers; domain logic stays in GameSession
  • The interface is defined in the layer that consumes it, not the layer that implements it

Banned Anti-Patterns

Every entry in this table has been considered and explicitly rejected. If you recognise one of these in existing code, open a refactor ticket — don’t extend it.

Pattern Decision Matrix

When you are unsure which pattern to reach for, use this table.

Coding Conventions

Naming, formatting, and class design rules that complement these patterns.

Testing Standards

How to test pattern-compliant code with NSubstitute mocks.

PR Checklist

The review checklist that enforces pattern compliance before merge.

Phase Breakdown

Which phases introduce which patterns first.