Skip to main content

Import boundaries

Read this when you add an import, a package or a feature, or change a Bazel visibility.

Visibility and package groups​

Bazel target visibility and the package groups in services/tadoku-api/BUILD.bazel (:feature_consumers, :domain_consumers, :infrastructure_consumers and :internal_consumers) define the local layer boundaries described in Code ownership.

  • Feature libraries are visible only to application, startup and E2E packages.
  • internal/tenant owns parsed tenant keys, context propagation and deployment scoping. It uses :internal_consumers and depends only on the standard library.
  • internal/tenant/alltenants exposes the restricted cross-tenant context marker. Its library is visible only to services/tadoku-api/app/worker, services/tadoku-api/features/jobqueue, services/tadoku-api/infra/postgres and services/tadoku-api/e2e in their exact Bazel packages. The queue owner maps its validated base scope to this marker; other features and the API transport cannot request it. A database context must carry either this marker or one tenant, never both.
  • The API application (app/api) is visible only to its binary (cmd/tadoku-api), the HTTP transport (transport/http) and E2E. The transport imports no other application.
  • The worker application (app/worker) is visible only to its binary (cmd/tadoku-worker) and E2E.
  • The owner-only lifecycle application (app/tenantlifecycle) is visible only to cmd/tadoku-tenant and E2E. Its generated sqlc package is visible only to that application; lifecycle SQL targets the tenant registry rather than an ordinary feature's business data.
  • A feature's generated sqlc package is visible only to that feature. The generated/sqlc/tokens package belongs only to features/authz/tokens.
  • Core logic in internal/ imports no feature, adapter or generated package; it reaches state through a port it declares. See Cross-cutting concerns.
  • app/permissionmodel, the list of every feature's permission namespace, is visible to the composition roots that build the model from it (cmd/tadoku-api, cmd/tadoku-worker, app/tenantlifecycle and E2E) and to app/worker for its tests only, so they build Relationships from the same model cmd/tadoku-worker injects. Bazel visibility cannot separate a package's tests from its library: the worker library must not import the model.
  • services/tadoku-api/infra/flipt owns the shared tenant-to-provider target mapping. Its ordinary consumers use :infrastructure_consumers; the exact test-only internal/testflipt fixture package may also depend on it so HTTP evaluation and management fixtures use that same mapping.
  • Tests follow their package's layer policy. Startup (cmd/tadoku-api) and E2E packages are assembly boundaries.
  • bazel build //services/tadoku-api/... checks the boundaries for every Go package and test it builds. rules_go requires direct imports to be declared in deps, and Gazelle's diff check keeps deps aligned with source imports.

Adding a package​

  • Gazelle resolves Go imports to Bazel deps and preserves existing visibility, but it creates new Go libraries as public. After running Gazelle, replace that default with the narrowest matching scoped visibility.
  • CI rejects public or out-of-service Tadoku API go_library visibility. Omitted visibility is Bazel-private.
  • Do not widen visibility to public merely to make a build pass.
  • When an import boundary intentionally changes, update the relevant target visibility or package group and this page in the same change, and explain the new dependency direction in the PR.

Profile and authz use the provider-free identity.Reader contract through their existing exact visibility exception. The generated read queries under generated/sqlc/identity/reads are visible only to identity. General cross-feature composition remains in the application.

Provider dependencies​

./tools/ci/check_tadoku_api_provider_deps.sh checks Bazel's direct dependency graph. Each allowed package covers only its own targets, not subpackages, and startup here means cmd/tadoku-api and cmd/tadoku-tenant:

  • Only features/leaderboard, features/identity (login throttling), features/profile (avatar cache), services/tadoku-api/infra/valkey, startup, E2E and the app/tenantlifecycle:tenantlifecycle_test target may depend directly on valkey-go.

Repository and store files​

A feature's service, repository and cache or store share one Go package, and Bazel visibility, the provider dependency check and depolicy all work per package. bazel run //tools/ci/repopolicy therefore checks the imports of each handwritten, non-test file under features/ by its file name:

  • *_cache.go and *_store.go may not import PostgreSQL packages: github.com/jackc/... (including pgx), generated sqlc packages or services/tadoku-api/infra/postgres/.
  • *_repository.go may not import provider clients: valkey-go, the Ory and Flipt SDKs, the service-token client (services/common/client/s2s/), or any package under services/tadoku-api/infra/ other than postgres, which includes the Flipt and Valkey clients.
  • Every other feature file, including the service, may not import valkey-go.

The rules follow Repositories and stores.

Verifying an import change​

Before publishing a PR that changes imports, packages or visibility, run from the repository root:

bazel run //:gazelle -- -mode=diff
./scripts/check-tadoku-api-visibility.sh
./tools/ci/check_tadoku_api_provider_deps.sh
bazel run //tools/ci/depolicy
bazel run //tools/ci/repopolicy
bazel build //services/tadoku-api/...

CI runs the same checks.

Depolicy backstop​

The depolicy check (bazel run //tools/ci/depolicy, configured in .depolicy.yaml) runs in CI as a temporary backstop. It scans every Go file, including tests and inactive build-tag files. Its YAML is not the source of truth for new package boundaries and does not define the preferred package structure. New internal packages must also be listed in its internal policy so their source and tests are covered.

Known gaps​

  • Visibility is owned by the imported target, so Bazel does not restrict Tadoku API imports from the retained public services/common infrastructure packages.
  • A normal build does not inspect Go files excluded by the active build configuration.
  • The CI visibility guard prevents a new public library but cannot tell whether its chosen scope matches its architectural role.
  • Import rules do not enforce same-package service and repository responsibilities or business signatures; those still need review.
  • The repository and store file check trusts file names and skips test files. A cache or repository in a file with another suffix gets only the valkey-go rule, and it cannot tell whether a repository and a cache in one package call each other.

Close or accept these gaps before relying on Bazel alone.