Skip to content

Project Structure

This guide explains how to organize a TinySystems module project for maintainability and scalability.

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.md

File 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
└── Dockerfile

Makefile

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=tinysystems

Go 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

Build flow-based applications on Kubernetes