This document records the key architectural decisions made for the multi-org DevOps blueprint. Each decision follows a lightweight ADR format to capture context, rationale, and consequences.
- ADR-001: Monorepo over Multi-Repo
- ADR-002: Trunk-Based Development for Shared Packages
- ADR-003: Path-Based CI Triggers over Org-Specific Branches
- ADR-004: GitHub Actions over Jenkins or Azure DevOps
- ADR-005: Custom Metadata Types for Org-Specific Configuration
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-01-15 |
We manage three Salesforce orgs (EU, US, APAC) that share approximately 60-70% of their metadata. The remaining 30-40% consists of region-specific customizations driven by compliance requirements (GDPR in EU), market differences, and localized business processes.
We needed to decide between:
- Multi-repo — one repository per org, with shared code copied or synced via submodules.
- Monorepo — a single repository containing shared packages and org-specific directories.
We chose a monorepo (federated model) with a clear separation between shared packages (packages/) and org-specific overrides (orgs/).
- Single source of truth. Shared business logic exists in exactly one place. Bug fixes and improvements propagate to all orgs through a single pull request, eliminating the need to open and synchronize multiple PRs across repos.
- Atomic cross-cutting changes. When a change to the data model (Core) requires a corresponding change to a service class (Logic) and an EU-specific layout, all three changes are committed, reviewed, and deployed together.
- Simplified dependency management. The dependency chain
core → integration → logic → org-specificis enforced structurally through directory conventions anddeployment-order.json, rather than through error-prone submodule pinning. - Easier onboarding. New developers clone one repository and can see the full picture — shared code, org overrides, and CI/CD pipelines — without navigating between repos.
- Path-based CI. GitHub Actions supports path-based triggers natively, allowing us to run only the relevant validation/deployment steps when specific directories change.
- Larger repository. The repo contains metadata for all orgs. Sparse checkouts or shallow clones may be needed for very large orgs.
- Shared CI minutes. A change to shared packages triggers validation across all orgs, consuming more CI minutes than a single-org repo would.
- Access control. Fine-grained permissions on subdirectories require GitHub CODEOWNERS rather than repo-level access control. Teams must be disciplined about code ownership boundaries.
- Merge contention. High-traffic shared packages may see more merge conflicts. Trunk-based development (see ADR-002) mitigates this.
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-01-20 |
We needed a branching strategy that supports both shared package development (where changes affect all orgs) and org-specific work (where changes affect only one org). Common options include:
- GitFlow — long-lived
develop,release/*, andhotfix/*branches with periodic merges tomain. - Trunk-based development — short-lived feature branches merged directly to
main, with all deployments triggered frommain. - Branch-per-org — a long-lived branch for each org (e.g.,
eu,us,apac) with cherry-picks or merges for shared code.
We adopted trunk-based development with main as the single integration branch. All changes — whether to shared packages or org-specific metadata — are developed on short-lived feature branches and merged to main via pull requests.
- Reduced merge complexity. GitFlow's long-lived branches accumulate drift and create painful merge conflicts, especially when three orgs are involved. Trunk-based development keeps the integration window short.
- Continuous deployment readiness. Every merge to
mainis a deployable unit. The path-based detection script determines which orgs and packages to deploy, makingmainthe single source of deployment truth. - No branch-per-org drift. Branch-per-org strategies inevitably diverge. Shared bug fixes must be cherry-picked to every org branch, and forgotten cherry-picks cause silent inconsistencies. Our directory-based separation eliminates this entirely.
- Simpler mental model. Developers always branch from
mainand merge back tomain. There is no question about which branch to target.
When to use feature branches:
- Always — but keep them short-lived (ideally < 1 day of work).
- Name them descriptively:
feature/gdpr-consent-update,fix/apac-routing-bug. - Do not create long-lived branches for org-specific streams.
- Feature flags may be needed. Large features that span multiple PRs require Custom Metadata Type-based feature flags to hide incomplete work from production.
- PR discipline is critical. Every PR to
mainmust pass validation. Branch protection rules enforce this. - Rollback strategy required. Since
mainis always deployable, a broken merge needs fast revert-and-redeploy capability. The smoke test script provides a safety net.
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-01-22 |
In a monorepo containing shared packages and three org directories, we need CI/CD to be efficient. Running full validation and deployment for every org on every commit is wasteful. We considered two approaches:
- Org-specific branches — a
deploy/eu,deploy/us,deploy/apacbranch for each org, with merges frommaintriggering org-specific pipelines. - Path-based triggers — a single
mainbranch with CI/CD workflows that usepaths:filters and a change detection script to determine which packages/orgs are affected.
We chose path-based CI triggers combined with the detect-changes.sh script that analyzes git diff output and maps changed files to affected packages and orgs.
- No branch synchronization overhead. Org-specific branches must be kept in sync with
main, creating maintenance overhead and a risk of drift. Path-based triggers eliminate this entirely. - Atomic deployments. A single merge to
maincan trigger both shared package deployments and org-specific deployments in one coordinated pipeline run. - Fine-grained control. The detection script can distinguish between changes to
packages/core/(deploy to all orgs) andorgs/eu/(deploy to EU only), enabling minimal-blast-radius deployments. - Native GitHub Actions support. The
on.push.pathsandon.pull_request.pathsfilters integrate directly with GitHub's event system, and the detection script provides additional granularity within a workflow. - Audit trail. Every deployment is traceable to a specific commit on
mainand a specific workflow run, regardless of which orgs were affected.
- Custom scripting required. The
detect-changes.shscript is a critical piece of infrastructure that must be maintained and tested. A bug in detection could skip necessary deployments or trigger unnecessary ones. - All-or-nothing for shared packages. When
sfdx-project.jsonor.github/files change, the detection script conservatively flags all packages and orgs, which may trigger more deployments than strictly necessary. - Workflow complexity. Path-based logic inside workflow YAML files (via matrix strategies and conditional steps) is more complex than simple branch-triggered pipelines.
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-02-01 |
We evaluated three CI/CD platforms for this blueprint:
- Jenkins — self-hosted, highly customizable, plugin-based.
- Azure DevOps Pipelines — cloud-hosted, strong enterprise features, YAML-based.
- GitHub Actions — cloud-hosted, native GitHub integration, YAML-based, marketplace.
We chose GitHub Actions as the CI/CD platform for this blueprint.
- Native GitHub integration. Since the repository is hosted on GitHub, Actions provides zero-configuration integration with pull requests, branch protection, environments, and secrets. PR checks, deployment gates, and status badges work out of the box.
- Workflow-as-code. Workflow definitions live in
.github/workflows/inside the repository, versioned alongside the code they deploy. There is no external pipeline configuration to manage or synchronize. - Path-based triggers. GitHub Actions natively supports
on.push.pathsandon.pull_request.pathsfilters, which are central to our change detection strategy (ADR-003). - Environments and approval gates. GitHub Environments allow us to define
eu-sandbox,production-eu, etc., with required reviewers and wait timers — without third-party plugins. - Community actions. The GitHub Marketplace provides well-maintained actions for Salesforce CLI installation, SFDX auth URL handling, Slack notifications, and more.
- Portability. While this blueprint uses GitHub Actions, the core scripts (
detect-changes.sh,smoke-test.sh) are platform-agnostic bash scripts. Migrating to another CI/CD platform requires only rewriting the workflow YAML files, not the underlying logic.
- GitHub lock-in for workflow syntax. The
.github/workflows/*.ymlfiles are GitHub-specific. However, the detection and deployment logic lives in portable shell scripts. - Runner minutes. GitHub-hosted runners have usage limits on free and team plans. Large organizations may need self-hosted runners or larger GitHub Enterprise plans.
- Limited built-in artifact management. Unlike Azure DevOps, GitHub Actions does not have a built-in artifact feed. We rely on workflow artifacts and the deployment history for traceability.
- Concurrent job limits. Free plans allow limited concurrent jobs. Fan-out deployments to three orgs may queue during peak usage.
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-02-10 |
Shared packages need to behave differently across orgs. For example, the routing service must direct cases to region-specific queues, and compliance handlers must be enabled/disabled based on the org's regulatory environment. We considered three mechanisms for org-specific configuration:
- Environment variables / Named Credentials — external configuration injected at deploy time or runtime.
- Custom Settings — Salesforce-native, hierarchy or list-based, editable via UI or API.
- Custom Metadata Types (CMDT) — Salesforce-native, deployable as metadata, version-controlled, available in declarative tools (formulas, validation rules, flows).
We chose Custom Metadata Types as the primary mechanism for org-specific configuration within shared packages.
- Deployable as metadata. CMDT records are metadata, not data. They can be version-controlled in the repository, deployed through CI/CD, and treated identically to other Salesforce metadata. This aligns perfectly with our source-driven approach.
- Available in declarative contexts. Unlike Custom Settings, CMDT records can be referenced in formula fields, validation rules, and flows without Apex code. This makes them accessible to admins and declarative developers.
- No SOQL governor limits. CMDT queries (
[SELECT ... FROM MyConfig__mdt]) do not count against SOQL query limits, making them safe to use in trigger contexts and batch operations. - Test isolation. CMDT records are visible in test contexts without
@SeeAllData=true, simplifying test setup and improving test reliability. - Natural fit for the federated model. Shared packages define the CMDT object (in
packages/core/), and each org provides its own records (inorgs/{region}/). This cleanly separates the schema from the configuration values.
Example pattern:
packages/core/objects/OrgConfig__mdt/ ← CMDT object definition (shared)
packages/core/objects/OrgConfig__mdt/fields/ ← Field definitions (shared)
orgs/eu/customMetadata/OrgConfig.EU.md-meta.xml ← EU-specific record
orgs/us/customMetadata/OrgConfig.US.md-meta.xml ← US-specific record
orgs/apac/customMetadata/OrgConfig.APAC.md-meta.xml ← APAC-specific record
- Cannot be modified at runtime via UI. CMDT records are metadata and are deployed, not edited in production via the Setup UI (unless using the Metadata API). For configuration that admins need to change frequently without a deployment, Custom Settings or Custom Permissions may be more appropriate.
- Schema changes require deployment. Adding a new field to the CMDT object requires a deployment to all orgs (since the object lives in a shared package). This is by design but adds coordination overhead.
- Limited data types. CMDT supports fewer field types than Custom Objects (e.g., no lookup to standard objects, no roll-up summaries). Complex configuration may require multiple CMDT objects.
- Mixed approach may be needed. Some configuration (e.g., API endpoints, credentials) is better handled through Named Credentials or Custom Settings. CMDT is the default choice, but not the only tool.
## ADR-NNN: [Title]
| Field | Value |
|-------|-------|
| **Status** | Proposed / Accepted / Deprecated / Superseded |
| **Date** | YYYY-MM-DD |
### Context
[What is the issue that we're seeing that is motivating this decision?]
### Decision
[What is the change that we're proposing and/or doing?]
### Rationale
[Why is this the best choice among the alternatives considered?]
### Consequences
[What becomes easier or harder as a result of this decision?]