| name | description |
|---|---|
dx-principles |
DevEx Principles: Fundamental guidelines for coding agents based on analysis of DevEx team code reviews and comments. These principles represent the core values and patterns that drive architecture decisions and code quality in the PagoPA DX initiative. |
This skill document captures the fundamental coding and architecture principles that emerge from the DevEx team's code review patterns and comments. These principles guide the creation of high-quality, maintainable, and secure code within the PagoPA DX initiative.
Analysis Basis: Mining of 43 substantive comments from 6 core team members (gunzip, lucacavallaro, mamu0, Krusty93, christian-calabrese, kin0992) across PRs and issue discussions in the dx repository.
Principle: For single-run inputs in scripts and GitHub Actions, prefer explicit arguments/parameters over environment variables.
Rationale:
- Environment variables are suitable for configuration that persists across multiple runs
- Arguments make intent explicit and single-run purpose clear
- Arguments are more discoverable and self-documenting
Application:
- GitHub Actions: Use action inputs instead of requiring environment variables to be set before invocation
- Shell scripts: Define explicit parameters rather than relying on ENV vars for transient values
- Terraform modules: Expose inputs as
variableblocks, not global environment configuration
Example Pattern:
# ❌ Improper: Relies on ENV var for single-run input
script.sh # expects OPEX_VERSION to be set
# ✅ Proper: Explicit argument
script.sh --opex-version=1.2.3
# GitHub Actions
# ❌ Improper:
env:
OPEX_VERSION: 1.2.3
# ✅ Proper:
with:
opex-version: '1.2.3'Principle: Always pin versions precisely; never allow latest or semantic version ranges in production dependencies.
Rationale:
- Floating versions (latest, ~, ^) introduce non-determinism and supply-chain risk
- Exact versions ensure reproducible builds and deployments
- Security: Compromised registries can't silently inject updates
Application:
- Terraform modules: Lock
required_versionand module versions to exact semver (e.g.,~> 2.0at most, specifying major and minor only) - GitHub Actions: Always use pinned tags/SHAs, never
@v1or@main - NPM dependencies: Use exact versions or conservative ranges (
~1.2.3) - MCP/Tool versions: Version alongside related packages for coherent evolution
Pattern:
# ❌ Improper
terraform {
required_version = ">= 1.0" # Too loose
}
# ✅ Proper
terraform {
required_version = "~> 1.6" # Major.minor pinned, patches allowed
}Principle: Keep the number of configurable parameters as small as possible; establish sensible defaults.
Rationale:
- Reduces cognitive load and implementation surface area
- Fewer parameters = fewer failure modes and simpler testing
- Makes tools more usable; users don't have to understand all options
- Decreases documentation burden
Application:
- Terraform modules: Provide limited, essential variables with intelligent defaults
- GitHub Actions: Keep
inputslist focused on truly configurable aspects - CLI tools: Prefer convention over configuration; make common paths automatic
- API endpoints: Provide reasonable defaults; expose advanced options only when necessary
Pattern:
# ❌ Improper: Too many optional parameters
variable "enable_monitoring" { default = false }
variable "enable_logging" { default = false }
variable "enable_alerts" { default = false }
variable "log_level" { default = "INFO" }
variable "retention_days" { default = 30 }
# ... 10+ more variables
# ✅ Proper: Unified, sensible defaults
variable "enable_observability" {
description = "Enable monitoring, logging, and alerting with defaults"
default = true
}Principle: Organize code in layers; each layer should only depend on the layer below it. Never leak implementation details upward.
Rationale:
- Clear separation of concerns makes code easier to reason about
- Prevents tight coupling and unintended dependencies
- Implementation changes don't cascade to higher layers
- Testability improves with isolated layers
Architecture Pattern:
3. Adapters/Infrastructure (external concerns: HTTP, databases, cloud providers)
2. Application/Use Cases (business logic and orchestration)
1. Domain (core entities and types)
Anti-Pattern:
- Use cases exposing cloud provider details (e.g.,
AzureResourceGroupin domain) - Controllers calling repositories directly; skipping business logic layer
- Infrastructure concerns (connection strings, API versions) bleeding into domain
Example:
// ❌ Leaked implementation detail
interface User {
id: string;
azureAdId: string; // Infrastructure detail in domain
}
// ✅ Proper abstraction
interface User {
id: string;
externalId: string; // Agnostic, can be any provider
}
// Mapping happens in adapters/Principle: Document not just code, but intent, use cases, and gotchas. Provide runnable examples.
Rationale:
- Reduces onboarding time and support burden
- Communicates design decisions and constraints
- Examples demonstrate correct usage patterns
Application:
- Terraform modules: Include README with use cases, examples/, and parameter descriptions
- APIs: Document not just endpoints, but when and why to use each one
- GitHub Actions: Explain inputs, outputs, and provide usage examples
- CLI: Include rich help text and example commands
Minimum Documentation:
- Purpose: What problem does this solve?
- Use Cases: When should this be used? When shouldn't it?
- Examples: Minimal reproducible example and real-world example
- Gotchas: Edge cases, limitations, security considerations
- Related: Links to related tools/modules
Principle: Treat secrets and sensitive configurations as first-class concerns. Use proper mechanisms throughout the lifecycle.
Rationale:
- Secrets in plaintext logs or state files create vulnerabilities
- Supply-chain attacks are a real threat
- Compliance and organizational policy require proper handling
Application:
- GitHub Actions: Use GitHub Actions Secrets, never hardcode placeholders
- Terraform: Use
sensitive = truefor outputs; externalize secret sources - Environment Variables: Only for non-sensitive config
- Type System: Make secret status part of the type signature
Pattern:
# ❌ Improper
variable "webhook_url" {
default = "placeholder" # Will overwrite real secret in Terraform
}
# ✅ Proper
variable "webhook_url" {
type = string
sensitive = true
# No default; sourced from external secret manager
}Principle: Maximize static guarantees and test coverage. Use strong typing and validation. Test edge cases.
Rationale:
- Type systems catch errors at author time, not runtime
- Tests validate assumptions and prevent regressions
- Failures are caught early and cheaply
Application:
- TypeScript: Strictly enforce typing; avoid
anyandastype assertions - Terraform: Validate variable types strictly; use
validationblocks for constraints - Testing: Test happy path, edge cases, error paths, and integration points
- Type Assertions: Avoid
aswhich bypasses type checking; use type guards instead
Pattern:
// ❌ Improper: Bypasses type checking
const value = something as string;
// ✅ Proper: Type guard
if (typeof something === "string") {
// TypeScript now knows it's a string
}Principle: Prefer functional composition and declarative patterns over imperative control flow.
Rationale:
- Functional patterns are easier to reason about and test
- Declarative code is more readable and less error-prone
- Pure functions improve composability and reusability
Application:
- Use
andThen/chaininstead of if/else for option/result handling - Prefer
map,filter,reduceover imperative loops - Embrace immutability; avoid mutation-based state management
Pattern:
// ❌ Improper: Imperative with side effects
if (result.isOk()) {
const value = result.value;
return transform(value);
} else {
return defaultValue;
}
// ✅ Proper: Functional composition
result.andThen((value) => transform(value)).getOrElse(defaultValue);Principle: Components that are released together should version together.
Rationale:
- Prevents version mismatch bugs
- Simplifies dependency management
- Clear contract and compatibility guarantees
Application:
- GitHub Actions and NPM packages: If tightly coupled, use the same version number
- Terraform modules and documentation: Version together if they evolve in lockstep
- CLI and inline scripts: Keep versions synchronized
Pattern:
# ✅ Good: Action versions match NPM package
# opex-dashboard package: v2.1.0
# opex-dashboard-generate action: v2.1.0
# Both released and versioned together via changesetPrinciple: Hide implementation details (cloud providers, HTTP libraries, etc.) behind abstraction layers.
Rationale:
- Switching providers/implementations becomes easier
- Core logic remains portable and testable
- Reduces cognitive load in business logic code
Application:
- Create adapter interfaces for external dependencies
- Business logic depends on interfaces, not concretions
- Multiple implementations can exist; testing uses mocks
Pattern:
// Domain/Application layer
interface AuthorizationService {
authorize(user: User, resource: Resource): Promise<boolean>;
}
// Adapters layer
class AzureAuthorizationAdapter implements AuthorizationService {
// Azure-specific implementation
}
class MockAuthorizationAdapter implements AuthorizationService {
// Testing implementation
}Principle: When documentation for Terraform modules exists, use official HashiCorp MCP tools to retrieve authoritative information.
Rationale:
- Terraform Registry is the source of truth
- Official tools ensure accuracy and reduce hallucinations
- API is reliable and well-maintained
Application:
- When documenting module parameters: Query Terraform Registry API
- When showing examples: Use official
.tffiles from module repositories - For updates: Always check Registry for latest versions and changes
Pattern:
# ✅ Proper
Use terraform-registry MCP tool → query module metadata → provide user accurate parameter documentation
# ❌ Improper
Assume or generate module documentation without consulting Registry
Principle: Names should be descriptive and self-documenting. Avoid abbreviations unless widely understood.
Rationale:
- Code is read more often than written
- Clear names reduce need for comments
- Misnamed things create bugs and confusion
Application:
- Variables: Use full words (
containerAppEnvironmentnotcae) - Functions: Name should describe what it does (
validateConfigurationnotcheck) - Modules: Clear purpose (
azure_container_appnotacr)
Principle: Minimize the attack surface of your supply chain. Treat external dependencies with caution.
Rationale:
- Compromised dependencies can execute arbitrary code
- Updates from "trusted" sources can be malicious
- The more dependencies, the larger the surface area
Application:
- Pin all versions (see Principle 2)
- Review dependency changes before merging
- Minimize number of external dependencies
- Be cautious of
@latestand auto-update mechanisms - Monitor security advisories for used packages
When reviewing or generating code, verify:
- Configuration uses arguments/inputs, not environment variables (for single-run)
- All versions are pinned to exact or conservative ranges (no
latest) - Parameter count is minimized with sensible defaults
- Architecture respects layering; no implementation detail leakage
- Documentation includes purpose, use cases, examples, and gotchas
- Sensitive data uses proper mechanisms (
sensitive = true, secrets management) - Type system is leveraged; no unnecessary
anyorasassertions - Control flow favors functional patterns over imperative if/else
- Related components are versioned together
- Implementation details are hidden in adapters/
- Terraform documentation sources from Registry when possible
- Names are clear and descriptive
- Supply chain risks are minimized (pinned versions, few dependencies)
See also: