Multi-Language Considerations¶
TL;DR¶
- Go is the default backend language for guide-conformant services. Every preceding chapter (Architecture, Data, Surface, Infra and Tooling, Observability, Security, Testing, Discipline, Air-gap) prescribes Go-side patterns. New backend code MUST start in Go unless an ADR justifies otherwise.
- TypeScript is the default browser language. The TypeScript chapter
(
10-typescript.md) prescribes the browser-first stack and scopes the Node.js TS subsection narrowly. - This chapter handles everything else by a small decision tree. The tree terminals are Python (data, ML, scripts), Bash (small orchestration with a strict length cap), YAML (cluster manifests, Helm v4 charts, GitOps), and case-by-case for any other language choice.
- Any new language introduced to a guide-conformant service MUST be recorded in an ADR. The ADR MUST cite the decision driver, the considered alternatives, and the support obligation the new language creates.
- Cross-language hygiene applies to every language choice: dependency pinning, supply-chain scanning (SBOM, vuln scanning), runtime version pinning, and observability parity (the new language's output MUST flow into the same logs / traces / metrics surface the Go and TS components use).
Why this choice¶
Most guide-conformant services need a small amount of code in a language other than Go or TypeScript: a data-prep script, a one-off migration runner, a cluster manifest, a CI orchestration helper. The question is not whether to allow non-Go-non-TS code — that fight is already lost the moment anyone needs to write a Bash one-liner. The question is how to bound the fragmentation so a service doesn't end up with six languages, four package managers, and three lint suites.
The decision tree below answers that question by partitioning the realistic non-Go-non-TS use cases into three named buckets — Python, Bash, YAML — and one explicit escape hatch (case-by-case with ADR discipline). Each named bucket has known-good tooling, known-good testing patterns, and a clear ceiling that signals when to migrate upward to Go or downward to nothing.
The choice to require an ADR for any other language follows from
the project-root principles in the Discipline chapter
(08-discipline.md): "modular by default" and "no backward
compatibility burden in pre-production" both depend on a stable
toolchain footprint. Adding Rust, Java, Ruby, or Kotlin to a
guide-conformant service is a multi-quarter support obligation; that
obligation MUST be visible to the team that takes it on.
Decision tree¶
The tree below MUST be evaluated top-down. The first matching terminal is the decision; later terminals MUST NOT be considered once an earlier one matches.
flowchart TD
Q1{Is this code part of the runtime<br/>request/response or operator path?}
Q1 -->|Yes| GO[Use Go.<br/>See Architecture / Data / Surface chapters.]
Q1 -->|No| Q2{Is this code rendered in the browser?}
Q2 -->|Yes| TS[Use TypeScript.<br/>See TypeScript chapter 10.]
Q2 -->|No| Q3{Is this code a Kubernetes /<br/>Helm / Crossplane manifest?}
Q3 -->|Yes| YAML[Use YAML.<br/>See Air-gap chapter 9 + Architecture Operator subsection.]
Q3 -->|No| Q4{Is this code a shell script<br/>under ~200 lines?}
Q4 -->|Yes| BASH[Use Bash.<br/>Hard length cap: ~200 lines.<br/>Beyond cap, migrate to Go or Python.]
Q4 -->|No| Q5{Is this code data preparation,<br/>ML training/eval, or scripting<br/>around a Python-only library?}
Q5 -->|Yes| PY[Use Python 3.12+.<br/>Pin with uv or poetry.<br/>SBOM via pip-audit.]
Q5 -->|No| Q6{Is there a compelling, ADR-justified<br/>reason for a different language?}
Q6 -->|Yes| CASE[Case-by-case.<br/>MUST write an ADR<br/>before writing the code.]
Q6 -->|No| STOP[Reject.<br/>Re-scope into Go, TS, YAML, Bash, or Python.]
The same tree in prose form (for chunk retrieval that cannot render the mermaid diagram):
- Is this code part of the runtime request/response or operator
path? If yes, the language MUST be Go. The Architecture
(
01-architecture.md), Data (02-data.md), and Surface (03-surface.md) chapters apply. - Is this code rendered in the browser? If yes, the language
MUST be TypeScript. The TypeScript chapter (
10-typescript.md) applies. - Is this code a Kubernetes, Helm, or Crossplane manifest? If
yes, the language MUST be YAML. The Air-gap chapter
(
09-airgap.md) and the Architecture chapter's Operator subsection apply. - Is this code a shell script under ~200 lines? If yes, Bash MAY be used, with a strict ~200-line ceiling. Scripts longer than the ceiling MUST be migrated to Go or Python.
- Is this code data preparation, ML training / evaluation, or
scripting around a Python-only library? If yes, Python 3.12
or later SHOULD be used, with dependency pinning via
uvorpoetry, and SBOM viapip-audit. - Is there a compelling, ADR-justified reason for a different language? If yes, the language choice MUST be recorded in an ADR before any code is written. The ADR MUST name the decision driver, the considered alternatives, and the multi-quarter support obligation the new language creates.
- Otherwise, the language proposal MUST be rejected and the work re-scoped into Go, TypeScript, YAML, Bash, or Python.
Prescriptive guidance per terminal¶
Python (data / ML / scripts)¶
- Python versions MUST be 3.12 or later (3.13 SHOULD be preferred at the 2026-05-08 snapshot). Older Python versions (3.10, 3.11) MUST NOT be introduced in new projects; existing code MAY remain on its pinned version until the next quarterly review surfaces a migration driver.
- Dependency pinning MUST use either
uv(recommended for new projects in 2026) orpoetry(acceptable for existing projects).pip install -r requirements.txtMUST be backed by a lockfile (requirements.txtgenerated bypip-compileor byuv pip compile); free-floatingpip installcalls in CI MUST NOT be used. - Lockfiles MUST be committed and refreshed on a quarterly cadence, not on every PR. The cadence keeps churn bounded and lets security review batch updates.
- Supply-chain scanning MUST run on every PR via
pip-audit(or equivalent). A known-vulnerable dependency MUST block merge unless a documented exception (ADR or risk acceptance) covers it. - Tests MUST be run via pytest. Test discovery MUST be deterministic;
glob-driven test discovery that pulls in scratch files MUST be
scoped (e.g.,
pytest tests/). - Type-checking MUST be configured (mypy in strict mode, pyright, or
pyrefly). Type errors MUST block merge in the same way Go vet errors
block merge in the Architecture chapter (
01-architecture.md). - Formatting MUST be standardized via ruff (which subsumes black, flake8, isort). The configured ruleset MUST be committed and enforced in CI.
- Observability output (logs, metrics) MUST flow into the same surface
the Go services emit to. The Observability chapter
(
05-observability.md) covers the OTLP and Prometheus shapes; the Pythonopentelemetry-instrumentation-*libraries MUST be used rather than rolling a bespoke shim. - Container packaging MUST follow the air-gap pattern from the
Air-gap chapter (
09-airgap.md): wheels MUST be pre-built and vendored into the deployment bundle, not pulled from PyPI at startup.
Bash (small orchestration with a hard length cap)¶
- Bash scripts MUST stay under approximately 200 lines including blank lines and comments. Beyond the cap, complexity outgrows what Bash's lack of structured types, error handling, and testing can support; the script MUST be migrated to Go or Python.
- Bash scripts MUST start with
#!/usr/bin/env bashandset -euo pipefail. The default Bash failure mode (errors silently continue) MUST NOT be relied on. - Bash scripts MUST be linted with shellcheck. CI MUST run shellcheck on every PR; flagged warnings MUST be resolved or suppressed inline with a documented reason.
- Bash scripts MUST use long-form flag names (
--verbose, not-v) for readability. Single-character flags MAY be retained for POSIX tools where they are idiomatic (grep -r). - Bash scripts MUST quote every variable expansion (
"$foo", not$foo). Unquoted expansion is the single largest source of defects in Bash and shellcheck flags it by default. - Bash scripts MAY shell out to other tools (
curl,jq,kubectl) but MUST validate the presence of each external dependency at the top of the script with acommand -vcheck. Missing dependencies MUST produce a helpful error, not a cryptic "command not found." - Bash scripts MUST NOT be used for stateful long-running processes. A background loop that maintains in-memory state belongs in Go or Python; Bash subshell semantics make state management brittle.
- Bash scripts SHOULD have at least a minimal
test/companion that exercises the script's--helpand dry-run paths. Hand-testing a Bash script with each PR is not a substitute for an automated smoke test.
YAML (Kubernetes, Helm, Crossplane, GitOps)¶
- YAML manifests MUST be the language for cluster-resident configuration: Kubernetes resources, Helm v4 chart templates, Crossplane Compositions, GitOps source-of-truth (Flux / Argo). Programmatic alternatives (Pulumi, CDK8s) MAY be used only with an ADR justifying the choice; the default is YAML.
- Helm v4 GA'd in November 2025 and MUST be the chart template engine baseline for new charts. Charts authored against Helm v3 MUST plan a migration in the next quarterly review.
- Crossplane v2 GA'd in late 2025 with five hard breaking API changes.
Compositions authored against Crossplane v1 MUST be migrated to v2
before being shipped in a new bundle; the Air-gap chapter
(
09-airgap.md) covers the bundle-build verification gate. - YAML manifests MUST be validated at build time (via
kubectl --dry-run=clientfor Kubernetes resources,helm lintfor charts, or schema validation for Crossplane CRDs). Build-time validation MUST block merge; runtime validation alone is too late. - YAML manifests SHOULD avoid inline shell scripts where possible.
Inline
command: ["/bin/sh", "-c", "long script ..."]blocks are unreviewable and bypass the Bash discipline above. Long logic belongs in a sidecar container with its source in a separate Go / Python / Bash file. - YAML manifests MUST use immutable image references (digest pins,
not floating
:latesttags). The Air-gap chapter (09-airgap.md) prescribes the cluster-local registry hostname rewrite that guarantees the digest is reachable. - Adopting orgs MAY use kustomize overlays for environment variations; MUST NOT maintain N hand-edited copies of the same manifest for N environments.
Case-by-case (ADR required)¶
- Any language other than Go, TypeScript, Python, Bash, or YAML introduced to a guide-conformant service MUST be recorded in an ADR before any code is written. The ADR MUST address the four cross-language hygiene questions below.
- The ADR MUST name the decision driver: what does this language solve that the existing toolchain cannot? "Performance" is acceptable only with a benchmark; "developer preference" is not.
- The ADR MUST list the considered alternatives explicitly including the option of staying in Go or Python.
- The ADR MUST commit to a support obligation: who owns upgrades, dependency scanning, security review, and onboarding documentation for the new language for the next four quarters at minimum.
- The ADR MUST identify the boundary: where does this language start and stop in the repository? A language whose scope creeps has effectively become a primary toolchain without the review attention that warranted.
Cross-language principles¶
The same hygiene applies to every language a guide-conformant service ships, whether it is Go, TypeScript, Python, Bash, YAML, or a case-by-case choice. The principles are not duplicative — they are the discipline that lets a multi-language service stay reviewable.
Dependency pinning¶
- Every language MUST commit a lockfile alongside its manifest:
go.sumfor Go,package-lock.json/pnpm-lock.yaml/bun.lockfor TS,uv.lockorpoetry.lockfor Python, chartrequirements.lockfor Helm. A lockfile that is gitignored is not a lockfile. - Lockfiles MUST be refreshed on a quarterly cadence, not on every PR. Frequent refresh causes review-noise without security gain.
- A dependency upgrade MUST be reviewed at least as carefully as a
feature change. The Security chapter (
06-security.md) covers the govulncheck / pip-audit / npm-audit gate that surfaces known vulnerabilities. - A pinned version MUST identify the exact upstream revision (semver
for ecosystems that publish semver, commit hash for ecosystems that
do not). Floating constraints (
*,latest,^any) MUST NOT be used in lockfiles.
Supply-chain hygiene¶
- Every language MUST produce a Software Bill of Materials (SBOM)
at build time, in a standard format (CycloneDX or SPDX). The SBOM
MUST be attached to the deployment bundle the Air-gap chapter
(
09-airgap.md) prescribes. - Every language MUST run a vulnerability scan on every PR. The scanner MAY vary by language (govulncheck for Go, npm-audit / pnpm-audit for TS, pip-audit for Python, trivy or grype for container images and YAML manifests). Findings MUST block merge unless explicitly accepted.
- Build-time signing of binaries and bundles MUST follow the same
signed-bundle pattern the Air-gap chapter (
09-airgap.md) prescribes. Per-language unsigned artifacts MUST NOT be deployed. - Third-party packages MUST be sourced from a vetted upstream (PyPI,
npm, Go module proxy, OCI registry). Curl-pipe-shell installation
patterns (
curl https://... | bash) MUST NOT be used in builds or in deployment scripts; they bypass every supply-chain control.
Runtime version pinning¶
- Every language MUST pin its runtime version at the repository
root:
go.mod'sgodirective for Go,package.json'sengines.nodefor Node,.python-versionorpyproject.toml'srequires-pythonfor Python,chart.yaml'skubeVersionfor Helm. - The pinned runtime MUST match the version CI runs against. A service pinned to Go 1.26 but built in CI against Go 1.24 will ship working binaries until it does not; the mismatch is the defect, not the deployment.
- Dockerfiles MUST consume the runtime version from the same source
of truth (e.g.,
golang:${GO_VERSION_FROM_GO_MOD}-alpine) rather than hardcoding a separate version literal. Drift betweengo.modand the Dockerfile is one of the most common air-gap smoke-test failures.
Observability parity¶
- Logs, metrics, and traces from non-Go-non-TS components MUST flow
into the same observability surface the Go and TS components use.
The Observability chapter (
05-observability.md) prescribes zerolog / OTLP / Prometheus shapes; cross-language libraries (opentelemetry-python, OTel Bash patterns viaotel-cli) MUST be used in preference to bespoke instrumentation. - Correlation IDs MUST propagate across language boundaries through
the standard W3C
traceparentheader. A Go service that calls a Python sidecar via HTTP MUST passtraceparentso the Python span is parented correctly. - A non-Go-non-TS component without observability output MUST NOT be considered production-ready. "Print to stdout" is acceptable for Bash scripts that exit quickly; long-running components MUST emit structured logs.
Testing parity¶
- Every language MUST have a CI test suite that runs on every PR.
The Testing chapter (
07-testing.md) covers Go-side discipline in depth; the language-specific tests (pytest,batsfor Bash,helm unittestfor charts) MUST follow the same "real backends, no mocks for transport boundaries" rule. - Test failures MUST block merge regardless of language. A Python script whose tests are skipped because "they're flaky on CI" is a defect against this chapter.
ADR discipline¶
- The first time a non-trivial slice of code in a new language enters
the repository, an ADR MUST land in the same PR. The ADR template
the Discipline chapter (
08-discipline.md) cross-references covers the required sections. - Subsequent additions in the same language MUST cite the ADR in the PR description so reviewers can ground the review in the agreed scope.
When to escalate up (consolidate languages) or down (delete)¶
A service that accumulates languages without bounds eventually owns more toolchain than it owns features. The signs that escalation is required:
- More than three Bash scripts in the same directory each over 100 lines — consolidate into a Go or Python CLI.
- A Python script with custom subprocess-management logic — that is a Go service trying to be born; promote it.
- A YAML manifest with inline shell logic over 20 lines — extract the logic into a sidecar container with proper source code.
- A "case-by-case" language whose footprint has grown past one directory — write a follow-up ADR formalizing it as a primary language, or migrate it back to Go/Python/TS.
The reverse signal — when to delete rather than consolidate — is equally important:
- A script no one has run in two quarterly reviews — delete it.
- A YAML manifest that no longer matches the running cluster — delete it before it confuses operators.
- A Python tool whose function has been absorbed by a Go binary — delete the Python tool, do not maintain the duplicate.
Industry context (informative)¶
- The Python ecosystem in 2026 has converged on uv (an
Astral-published Rust-implemented installer/resolver) for new
projects and poetry for established projects. Both are
acceptable;
pip install -r requirements.txtwith a manually curatedrequirements.txtMUST NOT be used as the only lockfile. - The Helm community released Helm v4 in November 2025. New charts MUST scaffold on v4; existing v3 charts MUST plan a migration in the next quarterly review.
- The Crossplane community released Crossplane v2 in late 2025
with five hard breaking API changes (composition function
signatures, package model, observability shape). The Air-gap
chapter (
09-airgap.md) covers the bundle-build gate that enforces Provider / Composition compatibility.
Reference Implementation: Pioneer
The donor codebase does not currently ship parallel Python or other
non-Go-non-TS services — its build-helper scripts live in Bash
under /home/ubuntu/pioneer/scripts/, its cluster manifests under
/home/ubuntu/pioneer/migrations/ (SQL) and operator deployment
bundles (YAML in Helm v3 charts on the embedded-registry side),
and its primary runtime languages are Go (backend, operator,
agent) and TypeScript (web frontend). Adopters MAY consult those
Bash scripts and YAML manifests for shape; the scripts demonstrate
the set -euo pipefail discipline and the manifests demonstrate
the digest-pinned image references this chapter prescribes.
If an adopting org adds a Python data-prep pipeline or an
ML evaluation harness, the ADR template under
docs/decisions/0000-template.md (a sibling document in this
guide, not a project-specific file) is the place to record the
language-introduction decision. The donor codebase does not have
such an ADR because the case has not yet arisen; an adopting org
will likely be the first to encounter it.
Pinned versions¶
Snapshot date: 2026-05-08. These pins represent the recommended minimums and ceilings for each named terminal of the decision tree. Quarterly reviews MUST re-confirm each entry.
| Terminal | Pinned version / constraint | Notes |
|---|---|---|
| Python interpreter | 3.12+ (3.13 preferred) | 3.10 and 3.11 are EOL-soon; new projects MUST start on 3.12+. |
uv (Python installer) |
latest stable | New projects SHOULD use uv; existing poetry projects MAY remain. |
poetry (Python installer) |
latest stable | Acceptable for existing projects; new projects SHOULD prefer uv. |
ruff |
latest stable | Subsumes black, flake8, isort; MUST be the formatter/linter. |
pytest |
latest stable | Test framework; required for every Python module. |
pip-audit |
latest stable | Supply-chain scan on every PR. |
| Bash | 5.0+ | Scripts MUST start with #!/usr/bin/env bash and set -euo pipefail. |
shellcheck |
latest stable | Required CI lint on every Bash file. |
| Helm | v4 GA (November 2025) | New charts MUST scaffold on v4. |
| Crossplane | v2 GA (late 2025) | New Compositions MUST be on v2 API. |
| YAML parser | language-specific | Validate at build time with kubectl --dry-run=client or helm lint. |
Pitfalls¶
- Sneaking in a new language without an ADR. A 50-line Rust helper "for performance" without an ADR commits the org to a multi-quarter support obligation invisible to most of the team. An ADR makes the obligation visible before the commitment is made.
- Bash scripts over 200 lines. Past the cap, structured types, proper error handling, and testability all become valuable enough that Go or Python is the right tool. Stretching Bash past the cap is a recurring source of long-tail defects.
- Free-floating
pip installin CI. Without a lockfile, the same CI run today and tomorrow can install different Python dependencies. The lockfile +uvorpoetrydiscipline prevents this. - Inline shell scripts in YAML manifests. Inline
sh -cblocks bypass shellcheck, hide test coverage, and degrade reviewability. Extract into a separate file in a sidecar container. - Floating image tags in YAML.
image: foo:latestdefeats the digest-pinning discipline the Air-gap chapter (09-airgap.md) prescribes. Image references MUST be digest-pinned. - Different Python versions in dev and CI. A script pinned to
Python 3.13 features but executed under Python 3.12 in CI ships
defects to staging that no one saw in dev. The
.python-versionfile MUST match CI's interpreter version. - Observability silence in a non-Go language. A Python ETL job that exits silently when it fails is invisible to the operator. Structured logs and OTel spans MUST be the default in every language.
See also¶
- The Architecture chapter (
01-architecture.md) for the Go-side layering pattern that the decision tree's "use Go" terminal points to. - The TypeScript chapter (
10-typescript.md) for the browser-first default that the decision tree's "use TypeScript" terminal points to, including the Node.js TS subsection that scopes Node usage narrowly. - The Air-gap chapter (
09-airgap.md) for the bundle-build, signed BOM, and digest-pinned image discipline that every multi-language service inherits. - The Discipline chapter (
08-discipline.md) for the project-root principles ("modular by default," "no backward compatibility burden in pre-production") that bound multi-language complexity. - The Security chapter (
06-security.md) for the vulnerability scanning toolchain (govulncheck / pip-audit / npm-audit / trivy / grype) that every language adopts. - The Observability chapter (
05-observability.md) for the OTLP and Prometheus shapes that every language's instrumentation MUST emit into. - The Testing chapter (
07-testing.md) for the real-backends rule that applies to every language's test suite. - The decisions subdirectory ADR template
(
docs/decisions/0000-template.md) for the MADR structure each language-introduction ADR MUST follow.