Go packages that implement the BloodHound Enterprise HTTP API and feature modules.
- Architecture diagrams
- Package structure
- The module system
- Layer architecture
- Adding a new feature module
- Interface design
- Testing
- Mock generation
- Code standards
LikeC4 source files live in docs/architecture/. They follow the C4 model and cover four levels of detail:
To view the diagrams:
Note: LikeC4 requires Node.js ≥ 22. Run
node --versionto confirm before installing.
# Install LikeC4 (if not already installed)
npm install -g likec4
# Serve interactive diagrams locally
cd bhce/server/docs/architecture
likec4 serve
# Or export to PNG
likec4 export png -o ./diagramsAvailable views:
- System Context (
index) – Who uses BHE and what external systems it connects to - Containers (
containers) – Deployable units: web UI, API server, databases - API Server Components (
apiServerComponents) – Go packages and feature modules - Analysis Internals (
analysisInternals) – Four-layer architecture within a feature - Type Imports (
analysisTypeImports) – Shows how handlers import services types, and appdb imports services errors (dependency inversion) - GET Request Flow (
analysisGetFlow) – Complete trace ofGET /api/v2/analysisthrough all layers - PUT Request Flow (
analysisPutFlow) – Complete trace ofPUT /api/v2/analysis, including idempotent insert - Shared Database Access (
sharedDatabaseAccess) – How multiple features independently access the same tables - Module Registration (
moduleRegistrationFlow) – Startup sequence and feature wireup
server/
├── modules/ # Shared Deps container and module registry
├── responses/ # Shared HTTP response helpers (envelopes, error wrappers)
├── docs/
│ └── architecture/ # LikeC4 (C4 model) source for the diagrams above
└── <feature>/ # One directory per vertical feature slice
├── <feature>.go # Register entry point
├── appdb/ # Persistence layer (SQL via go-sqlbuilder + pgx)
├── handlers/ # HTTP layer (handlers, routes, JSON views)
└── services/ # Business-logic layer (domain types, interfaces)
Each feature is a self-contained vertical slice. It owns every layer from HTTP to SQL; nothing bleeds across feature boundaries.
At startup, both lib/go/services/entrypoint.go and bhce/cmd/api/src/services/entrypoint.go call:
modules.Register(modules.Deps{
Router: &routerInst,
Pool: connections.RDMS.Pool(),
})modules.Deps is the shared dependency container; new cross-cutting infrastructure (graph database, filesystem, caches, etc.) is added to that struct so every feature module pulls from a single, consistent place.
modules.Register is the central dispatcher — it calls each feature module's Register function with the dependencies that module needs:
// server/modules/modules.go
func Register(deps Deps) {
analysis.Register(deps.Router, deps.Pool)
}Adding a feature is a one-line change in modules.go: import the new package and add a call to its Register function.
Every feature module follows a strict four-layer dependency chain assembled bottom-up inside its Register function:
HTTP request
│
▼
┌──────────────────────────────────────────┐
│ handlers (HTTP layer) │
│ – Defines the feature's interface |
│ – Auth, status codes, JSON marshalling │
└────────────────┬─────────────────────────┘
│ calls via interface
▼
┌──────────────────────────────────────────┐
│ services (business-logic layer) │
│ – Owns domain types │
│ – Defines the Database interface | │
│ – Maps storage errors to domain errors │
└────────────────┬─────────────────────────┘
│ calls via interface
▼
┌──────────────────────────────────────────┐
│ appdb (persistence layer) │
│ – Builds SQL with go-sqlbuilder │
│ – Executes via pgx │
│ – Returns services-layer sentinels │
└────────────────┬─────────────────────────┘
│ pgx pool
▼
PostgreSQL
The Register function wires the chain and registers routes. It takes only the infrastructure it directly needs from modules.Deps (the router and pgx pool today), making the dependency surface explicit:
// server/analysis/analysis.go
func Register(routerInst *router.Router, pool *pgxpool.Pool) {
var (
store = appdb.NewStore(pool)
svc = services.NewService(store)
handlerSet = handlers.NewHandlersContainer(svc)
)
handlers.Register(routerInst, handlerSet)
}Each layer receives only the layer below it. Layers never reach across or skip a boundary.
Follow these steps to add a new feature that fits the same pattern as analysis.
server/myfeature/
├── myfeature.go # Register entry point
├── appdb/
│ ├── appdb.go # Store struct + methods
│ └── appdb_test.go # Unit tests (pgxmock)
├── handlers/
│ ├── handlers.go # Handlers struct + MyFeature interface
│ ├── handlers_test.go # Unit tests (httptest)
│ ├── routes.go # Register(router, handlers)
│ └── views.go # JSON view types
└── services/
├── services.go # Service struct + domain types + Database interface
└── services_test.go # Unit tests (mock)
The services package owns domain types and sentinel errors. The Database interface lives here so the persistence layer depends on the consumer (Dependency Inversion). Add //go:generate go tool mockery so the mock is regenerated by just generate:
package services
//go:generate go tool mockery
type MyRecord struct { /* ... */ }
// Sentinel errors are defined here. The appdb layer returns these same errors
// so that handlers can use errors.Is() without importing appdb.
var ErrNotFound = errors.New("not found")
type Database interface {
GetMyRecord(ctx context.Context, id string) (MyRecord, error)
}
type Service struct{ db Database }
func NewService(databaseInterface Database) *Service { return &Service{db: databaseInterface} }Define the minimal pgxQuerier interface using only the pgx methods this store actually calls (each appdb package defines its own copy so the abstraction stays scoped to what is exercised here). Always use sqlbuilder.PostgreSQL to build queries, db: struct tags to map column names, and pgx.CollectOneRow/pgx.RowToStructByName to scan results. Return services-layer sentinels (not appdb-specific ones) so callers can use errors.Is without importing appdb:
package appdb
// pgxQuerier lists only the pgx methods this package actually calls.
// Add Query and/or Exec depending on what operations the store performs.
type pgxQuerier interface {
Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
}
// myRecord is the package-local DB row type. db: tags drive pgx.RowToStructByName scanning.
type myRecord struct {
ID string `db:"id"`
Name string `db:"name"`
}
// toMyRecord translates a raw DB row into the domain model.
func toMyRecord(row myRecord) services.MyRecord {
return services.MyRecord{ID: row.ID, Name: row.Name}
}
type Store struct{ db pgxQuerier }
func NewStore(db pgxQuerier) *Store { return &Store{db: db} }
func (s *Store) GetMyRecord(ctx context.Context, id string) (services.MyRecord, error) {
var (
rows pgx.Rows
row myRecord
err error
)
b := sqlbuilder.PostgreSQL.NewSelectBuilder()
b.Select("id", "name").From("my_table").Where(b.Equal("id", id))
sqlQuery, args := b.Build()
rows, err = s.db.Query(ctx, sqlQuery, args...)
if err != nil {
return services.MyRecord{}, err
}
row, err = pgx.CollectOneRow(rows, pgx.RowToStructByName[myRecord])
if errors.Is(err, pgx.ErrNoRows) {
return services.MyRecord{}, services.ErrNotFound
}
if err != nil {
return services.MyRecord{}, fmt.Errorf("reading rows: %w", err)
}
return toMyRecord(row), nil
}View types decouple the wire format from the domain model — the public API shape can evolve independently of internal domain changes, and vice versa. Each view struct has json: tags, a standalone BuildXxxView builder function that projects from the domain type, and a JSONView() method to satisfy responses.JSONViewer:
package handlers
import (
"encoding/json"
"github.com/specterops/bloodhound/server/myfeature/services"
)
// MyRecordView is the JSON shape returned by the handler. It is separate from
// services.MyRecord so the wire format can change without touching the domain model.
type MyRecordView struct {
ID string `json:"id"`
Name string `json:"name"`
}
// BuildMyRecordView projects a domain model into the view type.
func BuildMyRecordView(r services.MyRecord) MyRecordView {
return MyRecordView{ID: r.ID, Name: r.Name}
}
// JSONView satisfies responses.JSONViewer.
func (s MyRecordView) JSONView() ([]byte, error) {
return json.Marshal(s)
}The MyFeature interface is defined here (consumer side) to enable independent mock substitution in tests. Add //go:generate go tool mockery so the mock is regenerated by just generate. Each handler method reads from the request, calls the service, maps known sentinel errors to appropriate HTTP status codes, and uses the responses package to write the JSON envelope:
package handlers
//go:generate go tool mockery
import (
"context"
"errors"
"net/http"
"github.com/specterops/bloodhound/server/myfeature/services"
"github.com/specterops/bloodhound/server/responses"
)
type MyFeature interface {
GetMyRecord(ctx context.Context, id string) (services.MyRecord, error)
}
type Handlers struct{ feature MyFeature }
func NewHandlersContainer(feature MyFeature) *Handlers { return &Handlers{feature: feature} }
func (s Handlers) GetMyRecord(response http.ResponseWriter, request *http.Request) {
var ctx = request.Context()
record, err := s.feature.GetMyRecord(ctx, /* extract id from request */)
if errors.Is(err, services.ErrNotFound) {
responses.WriteError(ctx, http.StatusNotFound, "record not found", response)
return
}
if err != nil {
responses.WriteInternalServerError(ctx, err, response)
return
}
responses.WriteBasic(ctx, BuildMyRecordView(record), http.StatusOK, response)
}func Register(routerInst *router.Router, handlers *Handlers) {
permissions := auth.Permissions()
routerInst.GET("/api/v2/myfeature/:id", handlers.GetMyRecord).
RequirePermissions(permissions.AppReadApplicationConfiguration)
}The feature's Register accepts only the infrastructure it actually uses (here, the router and pgx pool):
package myfeature
import (
"github.com/jackc/pgx/v5/pgxpool"
"github.com/specterops/bloodhound/cmd/api/src/api/router"
"github.com/specterops/bloodhound/server/myfeature/appdb"
"github.com/specterops/bloodhound/server/myfeature/handlers"
"github.com/specterops/bloodhound/server/myfeature/services"
)
func Register(routerInst *router.Router, pool *pgxpool.Pool) {
var (
store = appdb.NewStore(pool)
svc = services.NewService(store)
handlerSet = handlers.NewHandlersContainer(svc)
)
handlers.Register(routerInst, handlerSet)
}In server/modules/modules.go, import the new package and call its Register from modules.Register:
import (
"github.com/specterops/bloodhound/server/analysis"
"github.com/specterops/bloodhound/server/myfeature" // ← new
)
func Register(deps Deps) {
analysis.Register(deps.Router, deps.Pool)
myfeature.Register(deps.Router, deps.Pool) // ← new
}If the new feature needs infrastructure that isn't on Deps yet (graph database, filesystem, caches, etc.), add the field to the Deps struct in modules.go and populate it from each entrypoint that calls modules.Register.
packages:
github.com/specterops/bloodhound/server/myfeature/services:
interfaces:
Database:
github.com/specterops/bloodhound/server/myfeature/handlers:
interfaces:
MyFeature:Then run just generate to produce the mock files.
Interfaces are always defined by the consumer, not the producer:
| Interface | Defined in | Implemented by | Purpose |
|---|---|---|---|
handlers.Analysis |
handlers/handlers.go |
services.Service |
Allows handler tests to swap in MockAnalysis |
services.Database |
services/services.go |
appdb.Store |
Allows service tests to swap in MockDatabase |
appdb.pgxQuerier |
appdb/appdb.go |
*pgxpool.Pool |
Allows store tests to swap in pgxmock |
Use pgxmock to mock the pgx pool. Assert exact SQL and argument values — use pgxmock.AnyArg() only when the value is genuinely non-deterministic at test time (e.g., time.Now()).
Use the generated MockDatabase. Pass concrete argument values to mock expectations; avoid mock.Anything.
Use the generated MockAnalysis. Capture responses with httptest.NewRecorder. Pass request.Context() to mock expectations rather than mock.Anything.
Carry the //go:build integration build tag and use pgtestdb for an isolated PostgreSQL instance.
go test -C bhce -tags integration ./server/myfeature/appdb/...Mocks are generated by mockery from bhce/.mockery.yml. After adding an interface, run:
cd bhce && just generateNever edit generated mock files by hand.
See bhce/AGENTS.md for the full list. Key points:
- Receiver functions on structs use
sas the variable name. - No named returns — all return variables declared inside the function body.
- Group
vardeclarations in avar ( ... )block, hoisted to the top of the function. - Use
anyinstead ofinterface{}. - Prefer descriptive variable names (
databaseInterfaceoverdb). - Test files testing only exported logic use the
_testpackage suffix. - Integration test files carry
//go:build integration(orserial_integration).