Skip to main content

Module Registration Reference

Complete reference for registering modules with the Constellation runtime, organizing custom standard libraries, and managing large module sets.

Quick Navigation

Registration Overview

Core Concepts

Module Registration is the process of making modules available to the Constellation runtime so they can be called from constellation-lang pipelines.

import cats.effect.IO
import cats.implicits._
import io.constellation._

// 1. Create a Constellation instance
val constellation = ConstellationImpl.init

// 2. Register modules
constellation.flatMap { c =>
c.setModule(myModule)
}

// 3. Use modules in pipelines

Key Registration Interfaces:

  • Constellation.setModule(module: Module.Uninitialized): IO[Unit] - Register a single module
  • ModuleRegistry - Internal registry managing name → module mappings
  • Module.Uninitialized - Module template ready for registration
  • Module.Initialized - Module with runtime context after initialization

Registration Workflow

┌─────────────────────┐
│ Define Module │
│ (ModuleBuilder) │
└──────────┬──────────┘


┌─────────────────────┐
│ Build Module │
│ (.build) │
└──────────┬──────────┘


┌─────────────────────┐
│ Register Module │
│ (setModule) │
└──────────┬──────────┘


┌─────────────────────┐
│ Use in Pipelines │
│ (constellation-lang)│
└─────────────────────┘

What Happens During Registration:

  1. Name Validation - Ensures module name is unique
  2. Spec Storage - Stores module spec in registry
  3. Factory Storage - Stores uninitialized module for later initialization
  4. Ready for Use - Module can now be referenced in DAG compilation

Single Module Registration

Basic Registration

Register a single module with the runtime:

import cats.effect.IO
import io.constellation._

case class TextInput(text: String)
case class TextOutput(result: String)

val uppercaseModule = ModuleBuilder
.metadata("Uppercase", "Convert text to uppercase", 1, 0)
.implementationPure[TextInput, TextOutput] { input =>
TextOutput(input.text.toUpperCase)
}
.build

// Register the module
val program: IO[Unit] = for {
constellation <- ConstellationImpl.init
_ <- constellation.setModule(uppercaseModule)
} yield ()

Registering with IO-Based Modules

For modules that perform side effects:

case class ApiInput(endpoint: String)
case class ApiOutput(data: String)

val apiCallModule = ModuleBuilder
.metadata("ApiCall", "Make HTTP API call", 1, 0)
.implementation[ApiInput, ApiOutput] { input =>
IO {
// Perform HTTP request
val response = scala.io.Source.fromURL(input.endpoint).mkString
ApiOutput(response)
}
}
.build

// Registration is the same
constellation.setModule(apiCallModule)

Registering Modules with Context

Modules can include definition context metadata:

import io.circe.Json
import io.circe.syntax._

val contextualModule = ModuleBuilder
.metadata("Contextual", "Module with context", 1, 0)
.definitionContext(Map(
"author" -> "John Doe".asJson,
"source" -> "custom-library".asJson,
"license" -> "MIT".asJson
))
.implementationPure[TextInput, TextOutput] { input =>
TextOutput(input.text.toUpperCase)
}
.build

constellation.setModule(contextualModule)

Error Handling During Registration

Registration can fail if module names conflict:

val safeRegistration: IO[Unit] = for {
constellation <- ConstellationImpl.init

// Check if module already exists
existing <- constellation.getModuleByName("Uppercase")

_ <- existing match {
case Some(_) =>
IO.println("Module already registered, skipping")
case None =>
constellation.setModule(uppercaseModule)
}
} yield ()

Batch Registration Patterns

Using traverse for Multiple Modules

The idiomatic way to register multiple modules in Cats Effect:

import cats.effect.IO
import cats.implicits._ // Required for .traverse
import io.constellation._

val modules: List[Module.Uninitialized] = List(
uppercaseModule,
lowercaseModule,
trimModule
)

val program: IO[Unit] = for {
constellation <- ConstellationImpl.init
_ <- modules.traverse(constellation.setModule)
} yield ()

Why traverse:

  • Transforms List[Module]List[IO[Unit]]IO[List[Unit]]
  • Executes registrations sequentially
  • Short-circuits on first error
  • Type-safe and composable

Registration with Error Accumulation

Continue registering modules even if some fail:

import cats.effect.IO
import cats.implicits._

def registerAllOrReport(
constellation: Constellation,
modules: List[Module.Uninitialized]
): IO[List[Either[Throwable, Unit]]] = {
modules.traverse { module =>
constellation.setModule(module).attempt
}
}

// Usage
val results = for {
c <- ConstellationImpl.init
results <- registerAllOrReport(c, modules)
_ <- IO.println(s"Registered ${results.count(_.isRight)} of ${modules.length} modules")
} yield results

Parallel Registration (Advanced)

Register independent modules in parallel for faster startup:

import cats.effect.IO
import cats.implicits._

val parallelRegistration: IO[Unit] = for {
constellation <- ConstellationImpl.init

// Register modules in parallel
_ <- modules.parTraverse(constellation.setModule)
} yield ()

⚠️ Caution:

  • Only use if module initialization is expensive and modules are independent
  • Module registry must be thread-safe (default implementation is)
  • Consider using bounded parallelism: parTraverseN(8)(constellation.setModule)

Conditional Registration

Register modules based on runtime conditions:

import cats.effect.IO

def registerConditionally(
constellation: Constellation,
modules: List[Module.Uninitialized],
enableFeatures: Set[String]
): IO[Unit] = {
val filtered = modules.filter { module =>
module.spec.metadata.tags.exists(enableFeatures.contains)
}

filtered.traverse(constellation.setModule).void
}

// Usage
val program = for {
c <- ConstellationImpl.init
_ <- registerConditionally(
c,
allModules,
Set("text", "math") // Only register text and math modules
)
} yield ()

Organizing Modules into Libraries

Category-Based Organization

Split modules into logical categories using traits (stdlib pattern):

package io.mycompany.modules

import io.constellation._

// modules/TextFunctions.scala
trait TextFunctions {
// Module definitions
val uppercaseModule: Module.Uninitialized = ...
val lowercaseModule: Module.Uninitialized = ...
val trimModule: Module.Uninitialized = ...

// Collect modules into a map
def textModules: Map[String, Module.Uninitialized] = Map(
uppercaseModule.spec.name -> uppercaseModule,
lowercaseModule.spec.name -> lowercaseModule,
trimModule.spec.name -> trimModule
)
}

// modules/MathFunctions.scala
trait MathFunctions {
val addModule: Module.Uninitialized = ...
val subtractModule: Module.Uninitialized = ...

def mathModules: Map[String, Module.Uninitialized] = Map(
addModule.spec.name -> addModule,
subtractModule.spec.name -> subtractModule
)
}

// MyLib.scala
object MyLib extends TextFunctions with MathFunctions {
def allModules: Map[String, Module.Uninitialized] =
textModules ++ mathModules
}

Benefits:

  • Clear separation of concerns
  • Easy to add/remove categories
  • Each category can be tested independently
  • IDE-friendly organization

Object-Based Organization

Group related modules in companion objects:

package io.mycompany.modules

import io.constellation._

object TextModules {
case class TextInput(text: String)
case class TextOutput(result: String)

val uppercase: Module.Uninitialized = ModuleBuilder
.metadata("Uppercase", "Convert to uppercase", 1, 0)
.implementationPure[TextInput, TextOutput](in => TextOutput(in.text.toUpperCase))
.build

val lowercase: Module.Uninitialized = ModuleBuilder
.metadata("Lowercase", "Convert to lowercase", 1, 0)
.implementationPure[TextInput, TextOutput](in => TextOutput(in.text.toLowerCase))
.build

val all: List[Module.Uninitialized] = List(uppercase, lowercase)
}

object DataModules {
val sumList: Module.Uninitialized = ...
val average: Module.Uninitialized = ...

val all: List[Module.Uninitialized] = List(sumList, average)
}

// Usage
val allModules = TextModules.all ++ DataModules.all

Multi-Package Organization

For large libraries, split into packages:

src/main/scala/io/mycompany/
├── MyLib.scala (top-level facade)
└── modules/
├── text/
│ ├── TextModules.scala
│ └── TextSignatures.scala
├── data/
│ ├── DataModules.scala
│ └── DataSignatures.scala
└── network/
├── HttpModules.scala
└── HttpSignatures.scala

MyLib.scala:

package io.mycompany

import io.constellation._
import io.mycompany.modules.text.TextModules
import io.mycompany.modules.data.DataModules
import io.mycompany.modules.network.HttpModules

object MyLib {
def allModules: Map[String, Module.Uninitialized] =
TextModules.all ++ DataModules.all ++ HttpModules.all

def registerAll(constellation: Constellation): IO[Unit] =
allModules.values.toList.traverse(constellation.setModule).void
}

Custom Stdlib Patterns

Creating a Domain-Specific Standard Library

Build a custom stdlib for a specific domain (e.g., e-commerce, finance, ML):

package io.mycompany.ecommerce

import cats.effect.IO
import cats.implicits._
import io.constellation._
import io.constellation.lang.LangCompilerBuilder
import io.constellation.lang.semantic._
import io.constellation.stdlib.StdLib

// 1. Define modules
trait ProductModules {
case class ProductInput(sku: String)
case class ProductOutput(name: String, price: Double)

val getProduct: Module.Uninitialized = ModuleBuilder
.metadata("GetProduct", "Fetch product by SKU", 1, 0)
.tags("ecommerce", "product")
.implementation[ProductInput, ProductOutput] { input =>
IO {
// Database lookup
ProductOutput("Widget", 29.99)
}
}
.build

def productModules: Map[String, Module.Uninitialized] = Map(
getProduct.spec.name -> getProduct
)
}

trait OrderModules {
case class OrderInput(userId: String, items: List[String])
case class OrderOutput(orderId: String, total: Double)

val createOrder: Module.Uninitialized = ModuleBuilder
.metadata("CreateOrder", "Create new order", 1, 0)
.tags("ecommerce", "order")
.implementation[OrderInput, OrderOutput] { input =>
IO {
OrderOutput("ORD-123", 99.99)
}
}
.build

def orderModules: Map[String, Module.Uninitialized] = Map(
createOrder.spec.name -> createOrder
)
}

// 2. Define function signatures for type checking
trait EcommerceSignatures {
val getProductSig = FunctionSignature(
name = "GetProduct",
params = List("sku" -> SemanticType.SString),
returns = SemanticType.SRecord(Map(
"name" -> SemanticType.SString,
"price" -> SemanticType.SFloat
)),
moduleName = "GetProduct"
)

val createOrderSig = FunctionSignature(
name = "CreateOrder",
params = List(
"userId" -> SemanticType.SString,
"items" -> SemanticType.SList(SemanticType.SString)
),
returns = SemanticType.SRecord(Map(
"orderId" -> SemanticType.SString,
"total" -> SemanticType.SFloat
)),
moduleName = "CreateOrder"
)

def ecommerceSignatures: List[FunctionSignature] = List(
getProductSig,
createOrderSig
)
}

// 3. Combine into a custom stdlib
object EcommerceStdLib
extends ProductModules
with OrderModules
with EcommerceSignatures {

def allModules: Map[String, Module.Uninitialized] =
productModules ++ orderModules

def allSignatures: List[FunctionSignature] =
ecommerceSignatures

// Register with compiler builder
def registerAll(builder: LangCompilerBuilder): LangCompilerBuilder =
allSignatures.foldLeft(builder)((b, sig) => b.withFunction(sig))

// Create a compiler with both base stdlib and ecommerce stdlib
def compiler: LangCompiler = {
val combinedModules = StdLib.allModules ++ allModules
val builder = registerAll(StdLib.registerAll(LangCompilerBuilder()))
.withModules(combinedModules)
builder.build
}
}

Usage:

val program = for {
// Use combined compiler
compiler <- IO.pure(EcommerceStdLib.compiler)

// Compile pipeline using both base and ecommerce functions
result <- compiler.compile("""
in sku: String

product = GetProduct(sku)

# Can also use base stdlib functions
formatted = concat("Product: ", product.name)

out formatted
""", "product-lookup")
} yield result

Extending Existing Stdlibs

Add domain modules while preserving the base stdlib:

object ExtendedStdLib {
// Your domain modules
val customModules: Map[String, Module.Uninitialized] = Map(
"CustomModule" -> myCustomModule
)

// Combine with base stdlib
def allModules: Map[String, Module.Uninitialized] =
StdLib.allModules ++ customModules

// Register all modules with Constellation
def registerAll(constellation: Constellation): IO[Unit] =
allModules.values.toList.traverse(constellation.setModule).void
}

Plugin-Based Architecture

Support loading modules from plugins:

trait ModulePlugin {
def name: String
def modules: Map[String, Module.Uninitialized]
def signatures: List[FunctionSignature]
}

class PluggableStdLib(plugins: List[ModulePlugin]) {
def allModules: Map[String, Module.Uninitialized] =
plugins.flatMap(_.modules).toMap

def allSignatures: List[FunctionSignature] =
plugins.flatMap(_.signatures)

def registerAll(builder: LangCompilerBuilder): LangCompilerBuilder =
allSignatures.foldLeft(builder)((b, sig) => b.withFunction(sig))
}

// Plugin implementation
object TextProcessingPlugin extends ModulePlugin {
def name = "text-processing"

def modules = Map(
"Uppercase" -> uppercaseModule,
"Lowercase" -> lowercaseModule
)

def signatures = List(uppercaseSig, lowercaseSig)
}

// Usage
val stdLib = new PluggableStdLib(List(
TextProcessingPlugin,
DataProcessingPlugin,
NetworkPlugin
))

Dynamic Module Loading

Loading Modules at Runtime

Load and register modules based on configuration:

import cats.effect.IO
import io.circe.parser._

case class ModuleConfig(
name: String,
enabled: Boolean,
tags: List[String]
)

def loadModulesFromConfig(configJson: String): IO[List[Module.Uninitialized]] = {
for {
config <- IO.fromEither(decode[List[ModuleConfig]](configJson))

// Map config to actual module instances
modules = config.filter(_.enabled).flatMap { cfg =>
ModuleRegistry.lookupByName(cfg.name)
}
} yield modules
}

val program = for {
config <- IO(scala.io.Source.fromFile("modules.json").mkString)
modules <- loadModulesFromConfig(config)
constellation <- ConstellationImpl.init
_ <- modules.traverse(constellation.setModule)
} yield ()

Lazy Module Registration

Register modules on-demand when first referenced:

import cats.effect.Ref

class LazyModuleRegistry(
constellation: Constellation,
moduleFactory: String => Option[Module.Uninitialized]
) {
private val registered = Ref.unsafe[IO, Set[String]](Set.empty)

def ensureRegistered(moduleName: String): IO[Unit] = {
registered.get.flatMap { reg =>
if (reg.contains(moduleName)) {
IO.unit
} else {
moduleFactory(moduleName) match {
case Some(module) =>
for {
_ <- constellation.setModule(module)
_ <- registered.update(_ + moduleName)
} yield ()
case None =>
IO.raiseError(new Exception(s"Unknown module: $moduleName"))
}
}
}
}
}

Hot Module Reloading

Support updating modules without restarting:

class ReloadableModuleRegistry(constellation: Constellation) {
def reloadModule(
name: String,
newModule: Module.Uninitialized
): IO[Unit] = {
// Note: Constellation doesn't support un-registration
// so this replaces the module implementation
constellation.setModule(newModule)
}

def reloadFromSource(
name: String,
sourceCode: String
): IO[Unit] = {
for {
// Compile new module from source
module <- compileModuleSource(sourceCode)

// Register (replaces existing)
_ <- constellation.setModule(module)

_ <- IO.println(s"Reloaded module: $name")
} yield ()
}
}

Module Versioning and Updates

Semantic Versioning

Use semantic versioning in module metadata:

val moduleV1 = ModuleBuilder
.metadata("MyModule", "Description", majorVersion = 1, minorVersion = 0)
.implementationPure[Input, Output] { input => ... }
.build

val moduleV2 = ModuleBuilder
.metadata("MyModule", "Description", majorVersion = 2, minorVersion = 0)
.implementationPure[Input, Output] { input =>
// Breaking change - new implementation
...
}
.build

Side-by-Side Versioning

Support multiple versions simultaneously with naming:

val moduleV1 = ModuleBuilder
.metadata("MyModule_v1", "Description v1", 1, 0)
.implementationPure[InputV1, OutputV1] { ... }
.build

val moduleV2 = ModuleBuilder
.metadata("MyModule_v2", "Description v2", 2, 0)
.implementationPure[InputV2, OutputV2] { ... }
.build

// Register both
constellation.setModule(moduleV1)
constellation.setModule(moduleV2)

In constellation-lang:

# Use specific version
result_v1 = MyModule_v1(input)
result_v2 = MyModule_v2(input)

Version Aliasing

Point "latest" alias to current version:

object ModuleVersions {
val v1: Module.Uninitialized = ...
val v2: Module.Uninitialized = ...

// Current production version
val latest: Module.Uninitialized = v2

def registerAll(constellation: Constellation): IO[Unit] = {
List(
v1,
v2,
latest.spec.copy(metadata = latest.spec.metadata.copy(name = "MyModule"))
).traverse(constellation.setModule).void
}
}

Deprecation Strategy

Mark deprecated modules with tags and context:

val deprecatedModule = ModuleBuilder
.metadata("OldModule", "DEPRECATED: Use NewModule instead", 1, 0)
.tags("deprecated", "legacy")
.definitionContext(Map(
"deprecated" -> true.asJson,
"deprecatedSince" -> "2024-01-01".asJson,
"replacement" -> "NewModule".asJson
))
.implementationPure[Input, Output] { ... }
.build

Namespace Management

Prefixed Naming

Use dot-notation for namespacing:

trait MathModules {
val add = ModuleBuilder
.metadata("stdlib.math.add", "Add two integers", 1, 0)
.tags("stdlib", "math")
.implementationPure[TwoInts, IntOut] { ... }
.build

val multiply = ModuleBuilder
.metadata("stdlib.math.multiply", "Multiply two integers", 1, 0)
.tags("stdlib", "math")
.implementationPure[TwoInts, IntOut] { ... }
.build
}

trait StringModules {
val concat = ModuleBuilder
.metadata("stdlib.string.concat", "Concatenate strings", 1, 0)
.tags("stdlib", "string")
.implementationPure[TwoStrings, StringOut] { ... }
.build
}

Benefits:

  • Clear ownership and category
  • Prevents name collisions
  • IDE autocomplete friendly
  • Easy to filter by prefix

Organization-Based Namespacing

Prefix modules with organization or team name:

// Team-specific modules
val orderModule = ModuleBuilder
.metadata("acme.sales.CreateOrder", "Create sales order", 1, 0)
.tags("acme", "sales")
.implementationPure[OrderInput, OrderOutput] { ... }
.build

val inventoryModule = ModuleBuilder
.metadata("acme.inventory.CheckStock", "Check inventory", 1, 0)
.tags("acme", "inventory")
.implementationPure[StockInput, StockOutput] { ... }
.build

Namespace Helpers

Build utilities for consistent naming:

object NamespaceHelper {
def namespaced(category: String, name: String): String =
s"mycompany.$category.$name"

def buildModule[I <: Product, O <: Product](
category: String,
name: String,
description: String
)(impl: I => O): ModuleBuilderInit = {
ModuleBuilder.metadata(
name = namespaced(category, name),
description = description,
majorVersion = 1,
minorVersion = 0
).tags("mycompany", category)
}
}

// Usage
val uppercase = NamespaceHelper
.buildModule[TextInput, TextOutput]("text", "Uppercase", "Convert to uppercase")
.implementationPure[TextInput, TextOutput](in => TextOutput(in.text.toUpperCase))
.build
// Results in module named: "mycompany.text.Uppercase"

Namespace Filtering

Register only modules matching a namespace:

def registerNamespace(
constellation: Constellation,
modules: Map[String, Module.Uninitialized],
namespace: String
): IO[Unit] = {
val filtered = modules.filter { case (name, _) =>
name.startsWith(s"$namespace.")
}
filtered.values.toList.traverse(constellation.setModule).void
}

// Register only "stdlib.math.*" modules
registerNamespace(constellation, allModules, "stdlib.math")

Module Discovery and Introspection

Listing Registered Modules

Query all registered modules:

val program = for {
constellation <- ConstellationImpl.init
_ <- allModules.traverse(constellation.setModule)

// List all registered modules
specs <- constellation.getModules

_ <- IO.println(s"Registered ${specs.length} modules:")
_ <- specs.traverse { spec =>
IO.println(s" - ${spec.name}: ${spec.description}")
}
} yield ()

Module Lookup by Name

Check if a specific module is registered:

val lookupModule = for {
constellation <- ConstellationImpl.init

// Try to get a specific module
maybeModule <- constellation.getModuleByName("Uppercase")

_ <- maybeModule match {
case Some(module) =>
IO.println(s"Found: ${module.spec.metadata.name}")
case None =>
IO.println("Module not found")
}
} yield ()

Query Modules by Tags

Filter modules by tags:

def findModulesByTag(
constellation: Constellation,
tag: String
): IO[List[ModuleNodeSpec]] = {
constellation.getModules.map { specs =>
specs.filter(_.metadata.tags.contains(tag))
}
}

// Usage
val textModules = for {
c <- ConstellationImpl.init
_ <- registerAll(c)
modules <- findModulesByTag(c, "text")
_ <- IO.println(s"Found ${modules.length} text modules")
} yield modules

Build Module Directory

Generate documentation from registered modules:

case class ModuleInfo(
name: String,
description: String,
version: String,
tags: List[String]
)

def buildModuleDirectory(
constellation: Constellation
): IO[List[ModuleInfo]] = {
constellation.getModules.map { specs =>
specs.map { spec =>
ModuleInfo(
name = spec.metadata.name,
description = spec.metadata.description,
version = s"${spec.metadata.majorVersion}.${spec.metadata.minorVersion}",
tags = spec.metadata.tags
)
}
}
}

// Generate JSON catalog
val catalog = for {
c <- ConstellationImpl.init
_ <- registerAll(c)
dir <- buildModuleDirectory(c)
json = dir.asJson.spaces2
_ <- IO(scala.io.File("module-catalog.json").write(json))
} yield ()

Health Check Modules

Verify all required modules are registered:

def checkRequiredModules(
constellation: Constellation,
required: Set[String]
): IO[Either[List[String], Unit]] = {
for {
specs <- constellation.getModules
registered = specs.map(_.metadata.name).toSet
missing = required -- registered
} yield {
if (missing.isEmpty) Right(())
else Left(missing.toList)
}
}

// Usage
val healthCheck = for {
c <- ConstellationImpl.init
_ <- registerAll(c)

result <- checkRequiredModules(c, Set(
"Uppercase",
"Lowercase",
"GetProduct"
))

_ <- result match {
case Right(_) =>
IO.println("✓ All required modules registered")
case Left(missing) =>
IO.raiseError(new Exception(s"Missing modules: ${missing.mkString(", ")}"))
}
} yield ()

Best Practices

Registration at Startup

Register all modules during application initialization:

object MyApplication extends IOApp {
def run(args: List[String]): IO[ExitCode] = {
for {
// Initialize Constellation
constellation <- ConstellationImpl.init

// Register all modules upfront
_ <- MyLib.registerAll(constellation)

// Verify registration
specs <- constellation.getModules
_ <- IO.println(s"Registered ${specs.length} modules")

// Start server
_ <- ConstellationServer.builder(constellation, compiler).run
} yield ExitCode.Success
}
}

Module Organization Checklist

For production deployments:

Group by Category - Use traits or objects to organize by domain ✅ Use Namespacing - Prefix module names with category/org ✅ Version Explicitly - Include version in metadata ✅ Tag Appropriately - Add tags for filtering and discovery ✅ Document Thoroughly - Write clear descriptions ✅ Test Registration - Verify all modules register successfully ✅ Health Checks - Check required modules at startup

Performance Considerations

Registration Performance:

  • Registration is fast (metadata storage only)
  • No need to optimize unless registering 1000s of modules
  • Consider lazy registration only if module initialization is expensive

Best Practices:

// ✅ Good: Register all at startup
modules.traverse(constellation.setModule)

// ❌ Avoid: Registering on every request
def handleRequest(moduleName: String) = {
constellation.setModule(getModule(moduleName)) // Don't do this
}

// ✅ Good: Register once, use many times
val app = for {
c <- ConstellationImpl.init
_ <- modules.traverse(c.setModule) // Once at startup

// Use registered modules in requests
_ <- handleRequests(c)
} yield ()

Error Handling Best Practices

Handle registration errors gracefully:

def registerWithErrorHandling(
constellation: Constellation,
modules: List[Module.Uninitialized]
): IO[Unit] = {
modules.zipWithIndex.traverse { case (module, idx) =>
constellation.setModule(module).handleErrorWith { error =>
IO.println(s"Failed to register module ${idx + 1}: ${module.spec.metadata.name}") *>
IO.println(s" Error: ${error.getMessage}") *>
IO.raiseError(error) // Re-raise to fail fast
}
}.void
}

Testing Module Registration

Unit test your module registration:

import munit.CatsEffectSuite

class ModuleRegistrationTest extends CatsEffectSuite {
test("all modules register successfully") {
for {
constellation <- ConstellationImpl.init
_ <- MyLib.allModules.values.toList.traverse(constellation.setModule)
specs <- constellation.getModules

// Verify count
_ = assertEquals(specs.length, MyLib.allModules.size)

// Verify all present
names = specs.map(_.metadata.name).toSet
_ = assert(names.contains("Uppercase"))
_ = assert(names.contains("Lowercase"))
} yield ()
}

test("can retrieve registered module by name") {
for {
constellation <- ConstellationImpl.init
_ <- constellation.setModule(MyLib.uppercaseModule)
maybeModule <- constellation.getModuleByName("Uppercase")

_ = assert(maybeModule.isDefined)
_ = assertEquals(maybeModule.get.spec.metadata.name, "Uppercase")
} yield ()
}
}

Monitoring Module Usage

Track which modules are actually used:

class MonitoredModuleRegistry(constellation: Constellation) {
private val usageCounts = Ref.unsafe[IO, Map[String, Long]](Map.empty)

def recordUsage(moduleName: String): IO[Unit] =
usageCounts.update { counts =>
counts.updated(moduleName, counts.getOrElse(moduleName, 0L) + 1)
}

def getUsageStats: IO[Map[String, Long]] =
usageCounts.get

def getUnusedModules: IO[List[String]] = {
for {
registered <- constellation.getModules
usage <- usageCounts.get

unused = registered
.map(_.metadata.name)
.filterNot(usage.contains)
} yield unused
}
}

Module Registry Patterns Summary

PatternUse CaseProsCons
Single RegistrationSmall apps, single moduleSimple, directDoesn't scale
Batch with traverseMedium apps (10-100 modules)Type-safe, composableSequential
Parallel RegistrationLarge apps (100+ modules)Fast startupRequires thread safety
Lazy LoadingOn-demand modulesMemory efficientComplex, race conditions
Plugin ArchitectureExtensible systemsHighly modularRequires plugin API
Namespace PrefixingLarge teamsPrevents collisionsVerbose names
Version AliasingMulti-version supportGradual migrationComplexity

Complete Examples

Minimal Example

import cats.effect.{IO, IOApp, ExitCode}
import cats.implicits._
import io.constellation._

object MinimalApp extends IOApp {
case class TextInput(text: String)
case class TextOutput(result: String)

val uppercaseModule = ModuleBuilder
.metadata("Uppercase", "Convert to uppercase", 1, 0)
.implementationPure[TextInput, TextOutput](in => TextOutput(in.text.toUpperCase))
.build

def run(args: List[String]): IO[ExitCode] = {
for {
constellation <- ConstellationImpl.init
_ <- constellation.setModule(uppercaseModule)
specs <- constellation.getModules
_ <- IO.println(s"Registered ${specs.length} module(s)")
} yield ExitCode.Success
}
}

Production Example

import cats.effect.{IO, IOApp, ExitCode, Resource}
import cats.implicits._
import io.constellation._
import io.constellation.lang.LangCompilerBuilder
import io.constellation.stdlib.StdLib

// 1. Define custom modules
object MyCompanyModules {
// Product modules
val getProduct = ModuleBuilder
.metadata("myco.product.Get", "Fetch product by SKU", 1, 0)
.tags("myco", "product")
.implementation[ProductInput, ProductOutput] { ... }
.build

// Order modules
val createOrder = ModuleBuilder
.metadata("myco.order.Create", "Create new order", 1, 0)
.tags("myco", "order")
.implementation[OrderInput, OrderOutput] { ... }
.build

val all: List[Module.Uninitialized] = List(
getProduct,
createOrder
)
}

// 2. Application setup
object MyApplication extends IOApp {
def setupConstellation: IO[Constellation] = {
for {
// Initialize
constellation <- ConstellationImpl.init

// Register stdlib
_ <- StdLib.allModules.values.toList.traverse(constellation.setModule)

// Register custom modules
_ <- MyCompanyModules.all.traverse(constellation.setModule)

// Verify registration
specs <- constellation.getModules
_ <- IO.println(s"✓ Registered ${specs.length} modules")

// Health check
_ <- checkRequiredModules(constellation, Set(
"myco.product.Get",
"myco.order.Create"
)).flatMap {
case Right(_) => IO.println("✓ All required modules present")
case Left(missing) => IO.raiseError(new Exception(s"Missing: ${missing.mkString(", ")}"))
}
} yield constellation
}

def checkRequiredModules(
constellation: Constellation,
required: Set[String]
): IO[Either[List[String], Unit]] = {
constellation.getModules.map { specs =>
val registered = specs.map(_.metadata.name).toSet
val missing = required -- registered
if (missing.isEmpty) Right(()) else Left(missing.toList)
}
}

def run(args: List[String]): IO[ExitCode] = {
setupConstellation.flatMap { constellation =>
// Run server or application logic
IO.println("Application ready") *> IO.pure(ExitCode.Success)
}
}
}

Multi-Environment Example

object EnvironmentAwareRegistry {
sealed trait Environment
case object Development extends Environment
case object Staging extends Environment
case object Production extends Environment

def getEnvironment: IO[Environment] = {
IO(System.getenv("APP_ENV")).map {
case "production" => Production
case "staging" => Staging
case _ => Development
}
}

def modulesForEnvironment(env: Environment): List[Module.Uninitialized] = {
val baseModules = StdLib.allModules.values.toList

val envSpecific = env match {
case Development =>
// Include debug modules in dev
baseModules ++ DebugModules.all

case Staging =>
// Include monitoring in staging
baseModules ++ MonitoringModules.all

case Production =>
// Production-optimized modules only
baseModules.filterNot(_.spec.metadata.tags.contains("debug"))
}

envSpecific
}

def setup: IO[Constellation] = {
for {
env <- getEnvironment
modules = modulesForEnvironment(env)
constellation <- ConstellationImpl.init
_ <- modules.traverse(constellation.setModule)
_ <- IO.println(s"Registered ${modules.length} modules for $env")
} yield constellation
}
}

Troubleshooting

Common Issues

Problem: Module not found in pipeline execution

Error: Unknown module 'MyModule'

Solution: Verify module is registered before compilation:

for {
c <- ConstellationImpl.init
_ <- c.setModule(myModule) // Must happen before compile

// Now compile pipeline
result <- compiler.compile(source, "my-dag")
} yield result

Problem: Name collision during registration

Error: Module 'Uppercase' already registered

Solution: Use unique names or namespacing:

// Option 1: Rename module
val uppercaseV2 = ModuleBuilder
.metadata("UppercaseV2", "Description", 2, 0)
.implementationPure[In, Out] { ... }
.build

// Option 2: Use namespace
val uppercaseText = ModuleBuilder
.metadata("text.Uppercase", "Description", 1, 0)
.implementationPure[In, Out] { ... }
.build

Problem: Module registered but types don't match

Error: Type mismatch for module 'MyModule'

Solution: Ensure FunctionSignature matches ModuleBuilder types:

// Module
case class MyInput(x: Long)
case class MyOutput(y: Long)
val module = ModuleBuilder
.metadata("MyModule", "Description", 1, 0)
.implementationPure[MyInput, MyOutput] { ... }
.build

// Signature MUST match
val signature = FunctionSignature(
name = "MyModule",
params = List("x" -> SemanticType.SInt), // Must match MyInput.x
returns = SemanticType.SInt, // Must match MyOutput.y
moduleName = "MyModule"
)

Problem: Forgot to import cats.implicits._

Error: value traverse is not a member of List[Module.Uninitialized]

Solution: Add the import:

import cats.implicits._  // Required for .traverse

modules.traverse(constellation.setModule)

Cross-Process Modules

All patterns above register in-process modules that run in the same JVM. For modules that need their own process, language runtime, or independent scaling, see the Module Provider Protocol:

  • External services register modules via gRPC instead of setModule
  • Modules can be written in any language (Python, Go, Rust, etc.)
  • Server handles namespace isolation, heartbeats, and load balancing
  • Trade-off: higher latency (network round-trip) for better isolation and scalability

See: Module Provider Integration

See Also