Active Object
Active Object Component — colocate everything about a concept together, structured by governance layers inside
Tags
Overview
Purpose
Active Object Component — colocate everything about a concept together, structured by governance layers inside
Context
Monorepo workspaces where entities span multiple concerns — types, adapters, processing logic, tests, fixtures, CLI commands. The system uses governance structure (FDD layers) internally but organizes by domain concept externally. Multiple consumers may share packages. The framework needs to discover and index components automatically.
Problem
How to organize code so that everything about a concept is discoverable,
modifiable, and deletable as a unit — without scattering related files
across technical-layer directories?
Forces:
(1) Scattering by technical layer (handlers/, types/, storage/) makes it
hard to understand a single concept end-to-end
(2) Deleting or refactoring a feature requires hunting across directories
(3) New developers cannot discover all parts of a concept without grep
(4) Internal structure still needs governance — adapters shouldn't import
domain types, tests need fixtures, etc.
(5) Shared packages must be reusable across multiple consumers
Solution
Group everything about a concept into one package/directory. Inside that directory, organize by FDD governance layers:
pkg/image/ ├── image.go # PD — domain types, interfaces, rules ├── resize.go # MD — processing logic ├── r2.go # SI — R2 storage adapter ├── handler.go # UI — HTTP handler / CLI command ├── image_test.go # Tests └── fixtures/ # Test data
The package IS the Active Object. It is: - Active: discoverable by the framework, indexed, has a registry entry - Typed: conforms to a known schema or protocol - Composable: other packages can import and use it without knowing internals
Internal dependencies follow FDD layer rules: handler imports processing, processing imports types, types import nothing from this package. External consumers import the package interface, not internal files.
Consequences
Benefits: - Concept comprehension: one directory = one complete understanding - Safe deletion: remove the directory, remove the feature entirely - Discoverability: framework indexes packages as components automatically - Onboarding: new developers find everything about a concept in one place - Refactoring confidence: all related code is colocated, nothing hidden
Liabilities: - Package size: large concepts produce large packages (mitigate by splitting into sub-concepts, each their own Active Object) - Cross-cutting concerns: logging, auth, metrics span multiple packages (keep these as shared infrastructure packages, not inlined) - Naming discipline: package name must reflect the concept, not the layer
Structure
Colocation rule:
- Package name = concept name (image, property, dictionary, knowledge)
- All files for that concept live in the package directory
- Internal structure follows FDD layers (PD types → MD logic → SI adapters → UI entry points)
- Tests and fixtures live alongside the code they test
What goes INSIDE the package:
- Domain types and interfaces (PD)
- Business logic and processing (MD)
- Storage/API adapters (SI)
- Handlers and CLI commands (UI) — when package-specific
- Tests and fixtures
What stays OUTSIDE (shared infrastructure):
- Cross-cutting middleware (auth, logging, metrics)
- Database connection management
- HTTP server setup
- Configuration loading
Implementation
In SBX Go monorepo:
pkg/{concept}/ # Shared domain packages
internal/{concept}/ # Internal implementation packages
Each package is self-contained:
pkg/property/
├── property.go # Types: Property, CreatePropertyInput
├── service.go # Business logic: Create, Get, Update
├── repository.go # Interface: PropertyRepository
├── postgres.go # SI: PostgresPropertyRepository
├── property_test.go # Tests
└── fixtures/ # Test data
Registry entry (system-component pattern):
.sbx/workspace/projects/sbx-dev/components/property/
└── component.yml # Links schema + manager + tests + fixtures
Discovery:
sbx describe shows packages as nodes in the workspace tree.
sbx framework index discovers components via registry.
Example
Consider an image processing package. Without colocation, image-related code scatters across the repo: the Go types land in pkg/types/, the upload handler in internal/handlers/, the R2 adapter in internal/storage/, the resize logic in internal/processing/, and the tests in tests/image/. When a developer needs to understand "how does image processing work?", they must search 5 directories. When they need to modify the upload flow, they touch files in 3 packages and hope they found everything. When they delete the feature, orphaned files remain in forgotten directories.
With Active Object colocation: pkg/image/ contains everything — types, adapter interface, R2 implementation, resize logic, handler, tests, fixtures. One directory answers "what does image do?" completely. Delete the directory, delete the feature. Add a new adapter, add it next to the existing one.
See Also
- layers: Internal package structure follows FDD layers — PD types at bottom, UI entry points at top. Active Object is the horizontal slice; layers are the vertical structure inside.
- aggregate: An Active Object package often corresponds to an aggregate boundary — the package contains the root entity and all its internal entities and value objects.
- bounded-context: Active Object packages within a project respect the project's bounded context — package names use the project's ubiquitous language.