Sound code design comes first. Then enforce six architecture principles in CI, not by convention.
Design principles
Write code that can change: SOLID, DRY, KISS, composition over inheritance, and the Law of Demeter. Treat these as guides, not laws. If two choices are equal, choose YAGNI. Add an abstraction when a real second case appears, not before.
Also watch for the opposite problem: an interface with one implementation, a strategy framework no one needs, or an abstraction built from three similar lines. Duplication costs less than the wrong abstraction. Learn these shapes so you can tell them apart:
- Design patterns, when a pattern earns its place.
- Code smells and refactoring techniques, refactoring.guru.
Keep business logic in pure, testable units with no I/O. The layering rule below enforces this separation.
Layering
Keep layers separate with one-way data flow. Keep business logic in pure, testable units. Follow ports-and-adapters: dependencies point inward, the domain core depends on nothing, and I/O stays at the edges. Test the dependency rule so violations fail the build. Use ArchUnit on the JVM, dependency-cruiser or eslint-plugin-boundaries for TypeScript, and import-linter for Python.
@AnalyzeClasses(packages = Array("com.ninjatrader"))
class LayeringTest {
@ArchTest val domainIsPure: ArchRule =
noClasses().that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAnyPackage("..infra..", "..web..")
}flowchart LR Adapters["Adapters: web, DB, queues"] -->|depend on| App[Application] App -->|depends on| Domain["Domain core, no I/O"]
Compiler strictness
Set the compiler to the strictest level the language allows. Type errors must fail the build. A warning suppression in new code needs a one-line reason. This is a quality gate, not a convention.
// build.sbt — Scala 2.13
scalacOptions ++= Seq(
"-Xfatal-warnings", "-Wunused:all", "-Wvalue-discard",
"-deprecation", "-feature", "-Xlint"
)// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true
}
}The equivalents are JDK -Werror -Xlint:all, Dart analysis_options.yaml with dart analyze --fatal-infos, and C# <Nullable>enable</Nullable> with <TreatWarningsAsErrors>true</TreatWarningsAsErrors>. See the Scala and TypeScript references.
Contracts and the domain model
Define the domain model once and generate everything downstream from it. The model lives in tradovate/master-scheme; never edit a file marked DO NOT CHANGE: THIS FILE WAS GENERATED by hand. Every API has a machine-readable contract: Protobuf/gRPC for internal services and OpenAPI for public REST. Generate clients and servers from that contract, which is the source of truth. Keep schema changes backward compatible: add-only fields, and reserve removed field numbers and enum values. A breaking-change check (Buf) enforces this in CI.
message Order {
string id = 1;
int64 quantity = 2;
reserved 3; // never reuse a removed field number
reserved "legacy_price";
optional string account_id = 4; // add-only, new tag
}Run generation in CI. Use a regenerate-and-diff check so generated code cannot become stale:
- name: Verify generated contracts are current
run: |
make generate
git diff --exit-code # fails if regeneration changed anythingIntegration
Choose synchronous RPC or async events deliberately. Use synchronous request/response when the caller needs an immediate, ordered answer (order entry, risk checks). Use async events (Pub/Sub) when eventual results are acceptable and you want decoupling. State which type each call uses. Make consumers idempotent and able to handle duplicate delivery. The Pekko-clustered trading backend documents its actor and delivery model explicitly.
Incremental migration
Migrate in small steps, with the new path beside the old one, instead of rewriting everything. Use the strangler fig pattern with an anti-corruption layer at the seam. Shift traffic with a feature flag. Define the “old path deleted” exit criterion first so the migration cannot stop halfway. Record the plan as an ADR or RFC.
Reuse
Use the shared building block before creating your own. Make the block easy to find through the monorepo modules or an internal library catalog.
Document the structure with the C4 model. Run these principles as fitness functions in CI/CD and testing.