| name | architectural-principles |
| description | Shared architectural and engineering principles for planning, implementing, and reviewing code. Single source of truth for SOLID, 12-factor, DRY, KISS, and defensive design. Referenced by Martian Manhunter, Cyborg, and Wonder Woman.
|
| user-invocable | false |
| disable-model-invocation | true |
| last_reviewed | "2026-04-28T00:00:00.000Z" |
Architectural Principles
These are the engineering principles that define what good software looks like in
this factory. Every agent that plans, implements, or reviews code references this
skill. When these principles are updated (manually or via Oracle), changes
propagate to all consuming agents automatically.
This skill defines the what — what good software looks like. Individual agent
skills (implementation-standards, review-criteria, planning-methodology) define
the how — how each agent applies these principles in their specific role.
SOLID Principles
Apply contextually based on your role:
Single Responsibility — Each module, class, or function should have one
reason to change. When planning: decompose tasks so each touches one concern.
When implementing: don't let a file grow to do two unrelated things. When
reviewing: flag files that mix concerns (e.g., a route handler that also
formats emails).
Open/Closed — Software entities should be open for extension but closed for
modification. Prefer adding new files over modifying existing ones when adding
new behavior. When the plan calls for "add a new type of X," check whether the
existing code has an extension point (plugin pattern, strategy pattern, registry)
before modifying the core.
Liskov Substitution — Subtypes must be substitutable for their base types.
If the codebase uses interfaces or abstract classes, new implementations must
honor the full contract — not just the method signatures, but the behavioral
expectations.
Interface Segregation — Don't force consumers to depend on methods they
don't use. Prefer small, focused interfaces over large ones. When planning API
endpoints, each endpoint should do one thing.
Dependency Inversion — Depend on abstractions, not concretions. When the
codebase uses dependency injection, follow that pattern. When it doesn't, don't
introduce it — but do keep high-level modules independent of low-level details
by using clear interfaces between layers.
12-Factor App Principles
Externalize configuration — Values that vary by environment (URLs, ports,
feature flags, API keys, thresholds) belong in environment variables or config
files, never hardcoded in source. If a value could be different in staging vs.
production, it's config.
Treat backing services as attached resources — Databases, caches, queues,
email services are swappable resources. Access them through configuration, not
hardcoded connection strings.
Store nothing in the process — Don't rely on in-memory state persisting
between requests. If state needs to persist, put it in a database or cache.
Logs as event streams — Write logs to stdout. Don't manage log files, log
rotation, or log shipping in application code.
Dev/prod parity — Keep development, staging, and production as similar as
possible. Don't use different databases, different queues, or different patterns
across environments.
DRY — Don't Repeat Yourself
When you see the same logic in two or more places, extract it — but only when the
duplication is real, not coincidental. Two pieces of code that happen to look
similar today but serve different purposes and will evolve differently are not
duplication. Three similar lines of code is better than a premature abstraction.
The test: If changing the logic in one place means you MUST change it in the
other place too (or risk a bug), it's real duplication. Extract it.
KISS — Keep It Simple
The simplest solution that meets the requirement is the right solution. Don't
add layers of abstraction "in case we need them later." Don't use a design
pattern because it's clever — use it because the code demands it.
Complexity is justified only when:
- The requirement is genuinely complex (not when the solution is over-engineered)
- The simpler alternative has a concrete, articulable drawback (not hypothetical)
- The complexity pays for itself in the current iteration (not in a future one)
Migration-First Mindset
Schema changes get migrations. Always. Never work around a schema change with
field mappings, SQL aliases, computed columns, or application-level transforms.
Migrations are not scary — they are the correct, reversible, auditable way to
evolve a schema.
When planning a task that touches data models:
- Include a migration file in the task's file list
- Include migration runner registration (npm scripts, CLI commands)
- Include type/schema updates that match the migration
When implementing: write the migration first, run it, then update the
application code to match.
When reviewing: if a schema change exists without a migration, flag it as
critical.
Configuration Over Hardcoding
Values that could vary by environment, change over time, or differ between
deployments go in configuration — not in source code. This includes:
- API URLs, ports, hostnames
- Feature flags and toggles
- Rate limits, timeouts, retry counts
- Email addresses, notification templates
- Lists of allowed/blocked values
The test: If changing this value requires a code change and redeployment,
it should be config instead.
Data-Driven Over Code-Driven
If something is a list of things that could change — categories, status values,
permission levels, supported formats — make it data, not an enum or switch
statement in code. Data can be updated without redeployment. Code requires a
release.
This doesn't mean every list needs a database table. A config file or constants
file that's easy to update is often sufficient. The point is: don't scatter
these values across business logic where they're hard to find and update.
Defensive Design
Validate at system boundaries — Every input from users, external APIs,
webhooks, or file uploads should be validated. Internal function calls between
trusted modules don't need redundant validation.
Handle edge cases — What happens with empty inputs? Missing data? Null
values? Zero-length arrays? Concurrent access? The code should handle these
gracefully, not crash.
Fail with useful errors — When something goes wrong, the error message
should tell the user (or developer) what happened, what they can do about it,
and where to look for more information. "Internal server error" helps nobody.
"Failed to connect to payment processor — check STRIPE_API_KEY is set" helps
everybody.
Never silently swallow errors — Catch blocks that ignore exceptions are
bugs. If you catch an error, either handle it meaningfully, re-throw it, or
log it. Empty catch blocks are never acceptable.