π UnitySwitch Backend Developer Constitution
Rule 1: The "Append-Only" Ledger Law (Database)
Rule: You are forbidden from using UPDATE or DELETE SQL statements on the transactions or ledger_entries tables.
- Why: BOZ requires a full, immutable audit trail. You cannot erase a mistake; you must correct it via a new entry.
- The Fix: If a transfer fails or is wrongly processed, your code must:
- Insert a new
REVERSAL transaction (Debit Receiver, Credit Sender).
- Set the
status column of the original transaction to REVERSED.
- Technical Enforcement: The database service account used by the Go backend must have only
INSERT and SELECT privileges. It is strictly forbidden to grant UPDATE or DELETE permissions on the ledger tables.
- See also Rule 14 β PII must NOT live directly in these immutable rows.
Rule 2: The Idempotency Mandate (Double-Payment Guard)
Rule: Every single payment endpoint must accept an X-Idempotency-Key header from the mobile app.
Rule 3: The "Two-Phase" Validation Rule (Balances & Race Conditions)
Rule: Always validate the sender's balance twice within a database transaction.
- Why: If two transfers happen at the exact same millisecond, standard balance checks will fail. We must lock the row to prevent double-spending.
- The Code Flow:
tx := db.Begin()
var balance int64
tx.Raw("SELECT balance FROM wallets WHERE user_id = ? FOR UPDATE", userID).Scan(&balance)
if balance < (amount + fee) {
tx.Rollback()
return errors.New("insufficient_balance")
}
tx.Exec("UPDATE wallets SET balance = balance - ? WHERE user_id = ?", (amount+fee), userID)
tx.Commit()
- Technical Enforcement: Any PR with a balance check that does not use
SELECT ... FOR UPDATE inside a DB transaction will be rejected immediately.
Rule 4: Timeout + Real Circuit Breaker (NFS Adapter) β Enhanced
Rule: Every call to the NFS or external banks must have a strict, hard-coded timeout of 3 seconds, and all calls must pass through an actual circuit breaker β not a timeout alone.
- Why a timeout isn't enough: A 3-second timeout makes each failed call slow instead of instant, but if NFS is degraded, you're still firing thousands of doomed 3-second calls per minute β that alone can exhaust your connection pool and goroutines. A real circuit breaker stops calling out entirely after N consecutive failures (open state), fails fast, and periodically tests recovery (half-open state).
- The Code:
import "github.com/sony/gobreaker"
var nfsBreaker = gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "nfs-adapter",
MaxRequests: 3,
Interval: 60 * time.Second,
Timeout: 30 * time.Second, // how long breaker stays OPEN before half-open
ReadyToTrip: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures > 5
},
})
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
resp, err := nfsBreaker.Execute(func() (interface{}, error) {
return sharedHTTPClient.Do(req.WithContext(ctx))
})
- Technical Enforcement: Developers are not allowed to use the default
http.DefaultClient, and no NFS call may bypass the shared breaker-wrapped client in /pkg/httpclient.
Rule 5: The "Trace ID" Law (Observability)
Rule: Every single log, API error, and database record must include a request_id (Trace ID).
- Why: When a customer calls support saying "My $500 didn't arrive," you need to search for their
user_nrc and instantly trace the path: Auth OK -> Balance Check -> NFS Called -> NFS Responded 200 -> Ledger Inserted.
- The Code: Always pass
context.Context through your function layers.
- Technical Enforcement: Use Go's standard
context.Context from the API Gateway to the DB layer. If a developer creates a new function and does not pass the ctx from the parent, they fail the code review.
Rule 6: The "Secrets" Rule (Zero Exposure)
Rule: You will never, ever, EVER commit a password, API key, or bank TLS certificate to GitHub.
- Why: If the repository is compromised, fraudsters can use your bank mTLS certificates to impersonate UnitySwitch and steal money.
- The Setup:
- Development: Local
.env file (in .gitignore).
- Production: Inject secrets via your cloud provider's Secret Manager (AWS Secrets Manager, Google Cloud Secret Manager) or Docker environment variables.
- Technical Enforcement: A GitGuardian Pre-Commit Hook (or similar) installed locally, scanning for hardcoded secrets and blocking the commit if found.
Rule 7: The "Bank-Grade" Logging Standard
Rule: Structured JSON logs only. fmt.Println is strictly prohibited.
- Why: Unstructured prints can't be searched in your monitoring dashboard (Datadog/NewRelic/ELK).
- The Code:
// BAD
fmt.Println("NFS failed for user 123")
// GOOD
logger.Error().
Str("request_id", reqID).
Str("user_nrc", nrc).
Str("error_type", "NFS_TIMEOUT").
Int("amount", 100).
Msg("External transfer failed")
- Technical Enforcement:
golangci-lint rule fails the build if fmt.Println or log.Println are detected.
Rule 8: The "Decoupled" Rule (No App-to-DB Direct Access)
Rule: The Flutter mobile app cannot talk to the Database directly.
- Why: Reverse-engineering the app to grab a DB connection string would let an attacker wipe transaction records.
- The Fix: The Flutter app talks only to the Go Backend REST endpoints; the Go Backend handles all queries via a hidden, privileged service role.
- Technical Enforcement: Strict RLS / Network ACLs accepting connections only from the Go Backend's private IP.
Rule 9: Money Is Always an Integer, Never a Float
Rule: All monetary values are stored and computed as int64 representing the smallest currency unit (ngwee β 1 ZMW = 100 ngwee). float32/float64/unconstrained NUMERIC are forbidden for any amount, balance, or fee field.
- Why: Floating-point rounding errors compound across millions of transactions. This is the single most common, most preventable bug class in payment systems, and it directly causes money to silently appear or vanish.
- The Fix:
type Transaction struct {
AmountNgwee int64 // never float64
FeeNgwee int64
}
Database columns: BIGINT, not FLOAT/REAL/DOUBLE PRECISION.
- Technical Enforcement: Schema review rejects any
FLOAT/REAL/DOUBLE PRECISION column on a money field. A custom lint rule flags float64 on any struct field tagged amount, balance, or fee.
Rule 10: A Timeout Is Not a Failure β Ambiguous NFS Outcomes Must Go to PENDING
Rule: Any NFS call that times out, errors at the network level, or returns an ambiguous response must set the transaction status to PENDING_VERIFICATION β never FAILED, and never auto-retried as a brand-new transaction.
- Why: Rule 4's timeout tells you the call didn't answer in time β it does not tell you whether NFS actually processed the debit. Marking it
FAILED and letting the user retry risks a double-debit if NFS in fact succeeded on its end. This is the real-world failure mode that causes actual customer fund loss.
- The Fix: A background worker polls NFS's status-check endpoint (idempotent
GET /transaction/{ref}) for any transaction stuck in PENDING_VERIFICATION, and only then resolves it to SUCCESS, FAILED, or triggers a REVERSAL (per Rule 1) if NFS confirms it never landed.
if err != nil {
tx.Exec("UPDATE transactions SET status = 'PENDING_VERIFICATION' WHERE id = ?", txnID)
// do NOT return "failed" to the user yet β queue a status check
enqueueStatusCheck(txnID)
return
}
- Technical Enforcement: Code review rejects any handler that sets
status = 'FAILED' directly inside an NFS-call error branch instead of PENDING_VERIFICATION.
Rule 11: Mandatory Settlement Reconciliation
Rule: A scheduled job must compare every PENDING/SUCCESS ledger entry against the corresponding NFS settlement report and flag mismatches.
- Why: NFS's settlement report is the actual source of truth for what moved. If your ledger silently drifts from it, you won't find out until a customer complains β or an auditor does. This is the mechanism that actually catches money lost "in the switch," not application logs.
- The Fix: Nightly (or as often as NFS publishes settlement files) job writes any discrepancy into a
reconciliation_exceptions table and pages on-call if exceptions exceed a threshold.
- Technical Enforcement: No production deploy is permitted without a green reconciliation run in the prior 24 hours.
Rule 12: Maker-Checker for Manual Money Movement
Rule: Any admin-initiated refund, ledger correction, or account freeze/unfreeze requires two distinct authenticated users β one to propose, one to approve.
- Why: This is the "four eyes" control BOZ auditors specifically check for. A single engineer or support agent able to unilaterally move money is both a fraud vector and a guaranteed audit finding.
- The Fix: Reject self-approval at the database constraint level, not just in application logic β e.g. a
CHECK (proposer_id <> approver_id) or equivalent trigger.
- Technical Enforcement: Any manual-intervention endpoint that executes on a single user's authorization fails code review outright.
Rule 13: AML/KYC Monitoring, Decoupled From the Payment Path
Rule: Every transaction is published asynchronously to a monitoring pipeline that checks velocity/threshold rules and flags anomalies β independent of whether the payment itself succeeds.
- Why: Inter-network mobile money movement is AML-regulated activity. If monitoring only runs inline with the happy path, a bug or edge case can silently skip it.
- Technical Enforcement: CI asserts that every new payment endpoint emits to the monitoring queue/topic; PR review checks for it explicitly.
Rule 14: PII Lives Outside the Immutable Ledger
Rule: transactions/ledger_entries store a tokenized reference ID, never raw PII (NRC number, phone number, full name) directly.
- Why: Rule 1's "never delete" is correct for financial records, but raw PII baked into permanently immutable rows conflicts with Data Protection Act obligations around data minimization and erasure requests. Separating PII into its own store lets you anonymize/purge it independently when legally required, without touching the financial audit trail.
- Technical Enforcement: Schema review rejects any new raw-PII column added directly to ledger tables.
Rule 15: Tested, Point-in-Time Recoverable Backups
Rule: Continuous WAL archiving / point-in-time recovery on Postgres, with a documented RPO/RTO and a quarterly restore drill into a sandbox.
- Why: An untested backup isn't a backup. For a ledger DB, data loss isn't an inconvenience β it's potentially unrecoverable customer funds.
- Technical Enforcement: Restore drill results are logged and reviewed each quarter; no exceptions.
Rule 16: Every Inbound NFS/Bank Callback Must Be Signature-Verified
Rule: Any asynchronous settlement/confirmation callback from NFS or a partner bank must be signature-verified (HMAC with shared secret, or mTLS) before its payload is trusted.
- Why: An unverified webhook is an open door for someone to fake a "payment succeeded" event.
- Technical Enforcement: Code review rejects any webhook handler that processes the payload before verifying the signature.
Rule 17: API Authentication & Authorization (Non-Webhook Endpoints)
Rule: All client-facing and internal endpoints (except inbound callbacks) must enforce strong authentication. No endpoint may default to anonymous access.
- Why: Rule 8 ensures no direct DB access, and Rule 16 secures callbacks. But without authentication on your own APIs, an exposed endpoint URL is enough to initiate payments or read sensitive data. Every user action must be tied to a verified identity and appropriate permissions.
- The Code:
- Validate a signed JWT or OAuth2 access token on every request.
- Reject expired or malformed tokens immediately with
HTTP 401.
- Admin endpoints must additionally enforce role-based access (RBAC) β e.g., only
approver role may finalize a maker-checker action.
- Technical Enforcement: A middleware layer is mandatory for all HTTP handlers. CI pipeline must include a security test that attempts an unauthenticated payment call and expects a
401. Any new route that lacks an auth middleware fails code review.
Rule 18: Rate Limiting & Brute-Force Protection
Rule: Every payment-related and authentication endpoint must enforce strict per-user (and per-IP) rate limits, configured to fail closed.
- Why: Idempotency keys prevent duplicate charges for identical requests, but nothing stops an attacker from sending a flood of different (valid) payments to drain a wallet in small increments or overwhelm the service. Real-time gating is required before money moves.
- The Fix:
- Implement a token-bucket or sliding-window rate limiter (e.g., via Redis).
- Default limit: 5 payment initiations per second per authenticated user; 20 per second per source IP.
- When breached, return
HTTP 429 Too Many Requests and do not process the request.
- An alert must fire if a single user/IP is repeatedly rate-limited.
- Technical Enforcement: Load test must demonstrate that rate limiting engages correctly and does not affect other users. PRs adding new endpoints must specify rate-limit rules and test them.
Rule 19: Data Encryption at Rest
Rule: All databases, file stores, and backups containing financial data, PII, or configuration must be encrypted at rest using AES-256 or stronger, with keys managed exclusively through a cloud KMS or Hardware Security Module (HSM).
- Why: A leaked disk snapshot or misconfigured storage bucket could expose wallet balances, tokenized PII mappings, and internal secrets. BOZ and the Data Protection Act mandate encryption of financial and personal data at rest.
- Technical Enforcement:
- Postgres: enable TDE or use filesystem-level encryption with KMS-managed keys.
- Backups: must be encrypted before leaving the instance; no plain-text dumps.
- Compliance check: run a quarterly automated scan that verifies no unencrypted storage volumes exist. Non-compliance blocks production release.
Rule 20: Proactive Monitoring & Alerting (SLIs/SLOs)
Rule: The service must expose core Service Level Indicators (SLIs) as Prometheus metrics and trigger PagerDuty/Opsgenie alerts on defined thresholds.
- Why: Structured logs and trace IDs give forensic capability, but they donβt wake you up at 2 AM. Without alerting, a degraded payment switch can bleed money or fail customers for hours.
- Required Metrics & Alert Thresholds:
payment_success_rate < 99.5% over 5 minutes β P1 page
circuit_breaker_state{name="nfs-adapter"} == 1 (open) for >5 minutes β P1 page
pending_verification_queue_depth > 100 items for >2 minutes β P2 page
reconciliation_exception_count > 0 β P2 page
http_request_duration_seconds p99 > 200ms for payment endpoint β P3 warning
- Technical Enforcement: Deployment pipeline must verify that the
/metrics endpoint is reachable and contains required metrics. Any new payment flow must expose its success/failure counters.
Rule 21: Performance & Load Testing Gates
Rule: Before any production release, a load test must prove that the payment endpoint sustains at least 2Γ the projected peak transactions per second (TPS) with p99 latency under 200ms (excluding the NFS call itself). Results are archived and compared; no deployment if performance degrades >10%.
- Why: A switch that works for 10 test users can collapse under real load. NFS likely has an SLA; our switch must not be the bottleneck. BOZ examiners will ask for capacity plans and test results.
- Technical Enforcement:
- Use k6, Vegeta, or Locust to run a standardized load profile in a staging environment.
- The CI/CD pipeline includes a gate:
Performance Regression Detected if p99 latency increases >10% from the last release.
- Load test must include idempotency key replay and realistic NFS mock latencies.
Rule 22: Immutable Audit Trail for Config & Admin Changes
Rule: Any change to system configuration, user roles, fee structures, blacklists, or NFS routing must be logged to an append-only admin_audit_log table. This table follows the same rules as the ledger: INSERT and SELECT only, no UPDATE or DELETE.
- Why: BOZ auditors will ask, βWho changed the fee structure on March 3rd and why?β If configuration changes silently overwrite previous values, you lose accountability for revenue-impacting and security-relevant decisions.
- The Data: Each row must capture:
actor_id, action (create/update/delete), table_name, record_id, old_values (JSON), new_values (JSON), ip_address, request_id, timestamp.
- Technical Enforcement: The DB user used for admin operations must not have
UPDATE/DELETE on admin_audit_log. Schema migration must add this table; code review ensures any admin panel or internal tool writes to it.
Rule 23: Mandatory BOZ Regulatory Reporting
Rule: A scheduled process must generate and securely archive daily regulatory reports required by the Bank of Zambia, including:
- Large-value transaction reports (above ZMW threshold defined by BOZ)
- Aggregate end-of-day settlement positions (per partner bank)
- Suspicious Activity Report (SAR) candidates based on velocity/structuring rules
- Why: βFull BOZ complianceβ isnβt just about safe codeβitβs about producing the actual files that regulators demand. Missing a report is a license-level violation.
- The Fix:
- Reports are generated as signed CSV/PDF files and stored immutably for a minimum of 5 years.
- Automated checks verify successful generation; if a report is not ready by 09:00 AM, a P1 alert fires.
- Technical Enforcement: The reconciliation smoke test in CI must also include a check that the reporting pipeline runs without errors. No production deploy without a successful daily report run in staging (simulated date).
π¨ CI/CD & PR Enforcement (Lead Dev's Toolbox)
- Branch Protection:
main/master is locked. Require Pull Request, no direct commits. Require 2 approvals.
- Pipeline Gates (GitHub Actions / GitLab CI):
- Lint:
golangci-lint (catches fmt.Println, float64 on money fields, bad practices).
- Tests: 100% coverage on payment orchestration logic (
go test -race ./...).
- Security Scan:
gosec for hardcoded credentials and insecure HTTP usage.
- Reconciliation Smoke Test: Reconciliation job runs against mock NFS settlement files in staging.
- Load Test Gate: Performance test passes (no >10% regression).
- Compliance Check: Encrypted storage verification, metrics endpoint check, report generation dry-run.
- The Golden Review Rule: If a PR uses
UPDATE on the ledger, stores raw PII on a ledger table, sets status = 'FAILED' directly on an NFS timeout, bypasses the breaker-wrapped HTTP client, allows self-approval on a manual intervention, lacks authentication middleware, omits rate limiting, adds unencrypted storage, or skips audit logging β block the merge. Protecting the money and the BOZ license is everyone's #1 priority.
Lead Dev's Final Word: The goal of this constitution is not to slow us down. It's to prevent a single bug from destroying our reputation β or our license. If the code passes these 23 rules, deploy with confidence.
PR Template Checklist
- - [ ] Rule 1: No UPDATE/DELETE on ledger tables
- - [ ] Rule 2: Idempotency keys implemented
- - [ ] Rule 3: SELECT ... FOR UPDATE used for balances
- - [ ] Rule 4: 3s timeout AND breaker-wrapped client used
- - [ ] Rule 5: Context with Trace ID passed through
- - [ ] Rule 6: Zero secrets committed
- - [ ] Rule 7: No fmt.Println, JSON logs only
- - [ ] Rule 8: App talks only to Go API, never DB
- - [ ] Rule 9: All money fields are int64 (ngwee), never float
- - [ ] Rule 10: NFS timeouts/errors set PENDING_VERIFICATION, not FAILED
- - [ ] Rule 11: Reconciliation job covers any new transaction type
- - [ ] Rule 12: Manual interventions require maker-checker approval
- - [ ] Rule 13: New payment endpoints emit to AML monitoring queue
- - [ ] Rule 14: No raw PII added to ledger tables
- - [ ] Rule 15: Backup/restore unaffected by schema change
- - [ ] Rule 16: Webhook handlers verify signature before processing
- - [ ] Rule 17: Auth middleware present; anonymous access blocked
- - [ ] Rule 18: Rate limiting configured and tested for payment/auth endpoints
- - [ ] Rule 19: New data stores encrypted at rest; keys via KMS
- - [ ] Rule 20: Metrics exposed and alert thresholds defined for new flows
- - [ ] Rule 21: Load test passes with no >10% latency regression
- - [ ] Rule 22: Admin/config changes recorded in immutable audit log
- - [ ] Rule 23: BOZ report generation pipeline updated for any schema change