Project Structure
This guide explains how to organize a TinySystems module project for maintainability and scalability.
Recommended Structure
my-module/
├── cmd/
│ └── main.go # Entry point
├── components/
│ ├── component1/
│ │ ├── component.go # Component implementation
│ │ ├── types.go # Message and settings types
│ │ └── component_test.go # Tests
│ ├── component2/
│ │ ├── component.go
│ │ ├── types.go
│ │ └── component_test.go
│ └── shared/ # Shared utilities (optional)
│ └── utils.go
├── pkg/ # Reusable packages (optional)
│ └── myutils/
│ └── helpers.go
├── Dockerfile
├── go.mod
├── go.sum
├── Makefile # Build automation
└── README.mdFile Responsibilities
cmd/main.go
The entry point imports all components and runs the CLI:
go
package main
import (
"github.com/tiny-systems/module/cli"
// Import all components to register them via init()
_ "github.com/myorg/my-module/components/router"
_ "github.com/myorg/my-module/components/filter"
_ "github.com/myorg/my-module/components/transform"
)
func main() {
cli.Run()
}components/*/component.go
Each component lives in its own package:
go
package router
import (
"context"
"github.com/tiny-systems/module/module"
"github.com/tiny-systems/module/registry"
)
const ComponentName = "router"
type Component struct {
settings Settings
}
// ... interface implementation ...
func init() {
registry.Register(&Component{})
}components/*/types.go
Keep type definitions separate for clarity:
go
package router
// Port message types
type Input struct {
Context any `json:"context" configurable:"true"`
Conditions []Condition `json:"conditions" required:"true"`
}
type Condition struct {
RouteName string `json:"routeName" required:"true"`
Condition bool `json:"condition" required:"true"`
}
// Settings
type Settings struct {
Routes []string `json:"routes" required:"true" minItems:"1"`
EnableDefaultPort bool `json:"enableDefaultPort"`
}
// Control port (if needed)
type Control struct {
Status string `json:"status" readonly:"true"`
}components/*/component_test.go
Test each component:
go
package router
import (
"context"
"testing"
)
func TestComponent_Handle(t *testing.T) {
component := &Component{}
component.settings = Settings{
Routes: []string{"success", "failure"},
}
var capturedPort string
var capturedData any
handler := func(ctx context.Context, port string, data any) any {
capturedPort = port
capturedData = data
return nil
}
// Test routing to first matching condition
input := Input{
Context: map[string]string{"key": "value"},
Conditions: []Condition{
{RouteName: "success", Condition: true},
},
}
result := component.Handle(context.Background(), handler, "input", input)
if result != nil {
t.Errorf("expected nil, got %v", result)
}
if capturedPort != "out_success" {
t.Errorf("expected out_success, got %s", capturedPort)
}
}Naming Conventions
Components
- Package name: lowercase, single word (e.g.,
router,filter) - Component name: Same as package, used in TinyNode spec
- Const for name:
ComponentName = "router"
Ports
- Input ports: Descriptive names (
input,request,data) - Output ports: Prefixed with action or destination (
out_success,response) - Settings port: Use
v1alpha1.SettingsPort - Control port: Use
v1alpha1.ControlPort
Types
- Input types: Suffix with purpose (
RequestInput,DataInput) - Output types: Suffix with purpose (
ResponseOutput,ResultOutput) - Settings: Named
Settings - Control: Named
Control
Multi-Component Module Example
Here's the structure of the common-module:
common-module/
├── cmd/
│ └── main.go
├── components/
│ ├── router/
│ │ ├── component.go # Conditional routing
│ │ └── types.go
│ ├── split/
│ │ ├── component.go # Array iteration
│ │ └── types.go
│ ├── modify/
│ │ └── component.go # Pass-through
│ ├── async/
│ │ └── component.go # Non-blocking execution
│ ├── ticker/
│ │ ├── component.go # Periodic emission
│ │ └── types.go
│ ├── delay/
│ │ └── component.go # Timed delay
│ ├── signal/
│ │ ├── component.go # Manual trigger
│ │ └── types.go
│ └── debug/
│ └── component.go # Message sink
├── go.mod
└── DockerfileMakefile
Automate common tasks:
makefile
.PHONY: build test run docker
MODULE_NAME := my-module
VERSION := 1.0.0
build:
go build -o $(MODULE_NAME) ./cmd
test:
go test ./... -v
run: build
./$(MODULE_NAME) run \
--name=$(MODULE_NAME) \
--version=$(VERSION) \
--namespace=default
docker:
docker build -t myregistry/$(MODULE_NAME):$(VERSION) .
push: docker
docker push myregistry/$(MODULE_NAME):$(VERSION)
info: build
./$(MODULE_NAME) tools info
lint:
golangci-lint run ./...
clean:
rm -f $(MODULE_NAME)Dockerfile
Optimized multi-stage build:
dockerfile
# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
# Cache dependencies
COPY go.mod go.sum ./
RUN go mod download
# Build
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o module ./cmd
# Runtime stage
FROM alpine:3.19
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/module .
# Run as non-root
RUN adduser -D -u 1000 appuser
USER appuser
ENTRYPOINT ["./module"]
CMD ["run"]Environment Configuration
Common environment variables:
bash
# Kubernetes (usually auto-configured in cluster)
KUBERNETES_SERVICE_HOST=kubernetes.default.svc
KUBERNETES_SERVICE_PORT=443
# OpenTelemetry (optional)
OTLP_DSN=http://otel-collector:4317
# Pod identification (set by Kubernetes)
HOSTNAME=my-module-abc123
POD_NAMESPACE=tinysystemsGo Module Dependencies
Typical go.mod:
go
module github.com/myorg/my-module
go 1.21
require (
github.com/tiny-systems/module v0.1.171
)The SDK pulls in all necessary Kubernetes and gRPC dependencies.
Next Steps
- Component Model - Understand the component lifecycle
- Testing Components - Write comprehensive tests
- Building Modules - Prepare for deployment