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/tenantowns parsed tenant keys, context propagation and deployment scoping. It uses:internal_consumersand depends only on the standard library.internal/tenant/alltenantsexposes the restricted cross-tenant context marker. Its library is visible only toservices/tadoku-api/app/worker,services/tadoku-api/features/jobqueue,services/tadoku-api/infra/postgresandservices/tadoku-api/e2ein 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 tocmd/tadoku-tenantand 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/tokenspackage belongs only tofeatures/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/tenantlifecycleand E2E) and toapp/workerfor its tests only, so they buildRelationshipsfrom the same modelcmd/tadoku-workerinjects. Bazel visibility cannot separate a package's tests from its library: the worker library must not import the model.services/tadoku-api/infra/fliptowns the shared tenant-to-provider target mapping. Its ordinary consumers use:infrastructure_consumers; the exact test-onlyinternal/testfliptfixture 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_gorequires direct imports to be declared indeps, and Gazelle's diff check keepsdepsaligned with source imports.
Adding a package
- Gazelle resolves Go imports to Bazel
depsand 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_libraryvisibility. Omitted visibility is Bazel-private. - Do not widen visibility to
publicmerely 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 theapp/tenantlifecycle:tenantlifecycle_testtarget may depend directly onvalkey-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.goand*_store.gomay not import PostgreSQL packages:github.com/jackc/...(including pgx), generated sqlc packages orservices/tadoku-api/infra/postgres/.*_repository.gomay not import provider clients:valkey-go, the Ory and Flipt SDKs, the service-token client (services/common/client/s2s/), or any package underservices/tadoku-api/infra/other thanpostgres, 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/commoninfrastructure 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-gorule, 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.