Service — The Container

Service Configuration

Add typed custom configuration with Zod schemas

A custom service configuration is service-specific settings made available to all commands and subscriptions via this.config. It is not used by PURISTA itself — it is for your business logic.

Custom configs are one way to pass values to handlers. You can also use config stores, secret stores, or state stores. The right choice depends on what the data is, who manages it, and how often it changes.

When to use what

Use this decision tree to choose the right mechanism:

Custom config when:

  • The value is set once during deployment (API URLs, ports, timeouts).
  • The value rarely changes (database connection pool size).
  • The value is infrastructure-related, not business-related.

Config store when:

  • The value changes during runtime (feature flags, currency rates).
  • Business users or automation need to update it.
  • The value is not confidential.

Secret store when:

  • The value is confidential (passwords, tokens, certificates).
  • It must never appear in code, logs, or environment files.

State store when:

  • The value is temporary runtime state (session data, caches).
  • It needs fast read/write access.

Config vs stores

Custom configConfig storeSecret store
Provided byInfrastructure & deploymentDatabase or vendor solutionVendor solution
AddressesTechnical configurationBusiness configurationSecrets & confidential data
Value lifetimeSet once at instance creationFetched per usageFetched per usage
Changes applyOn restart / next deploymentOn next usageOn next usage
Value typeObject (nested)Key-value (scalar)Key-value (string)
Can be updated by handlersNoYesYes
Safe for secretsNoNoYes
Use casesThird-party URLs, ports, timeoutsFeature flags, currency exchange ratesPasswords, auth tokens, certificates

Define a config schema

Custom configurations require a Zod schema:

import { z } from 'zod'

export const userServiceV1ConfigSchema = z.object({
  apiTimeout: z.number().optional().default(5000),
  featureFlagNewUI: z.boolean().optional().default(false),
  externalApiUrl: z.string().url(),
})

export type UserServiceV1Config = z.input<typeof userServiceV1ConfigSchema>

Attach the schema to the service builder:

export const userServiceV1ServiceBuilder = new ServiceBuilder(userServiceInfo)
  .setConfigSchema(userServiceV1ConfigSchema)

Optional fields with defaults are safe: if no value is provided during instance creation, the default is used automatically. Required fields without defaults will cause a type error if missing.

Provide config at startup

const userService = await userServiceV1Service.getInstance(eventBridge, {
  logger,
  resources,
  serviceConfig: {
    apiTimeout: 10000,
    featureFlagNewUI: true,
    externalApiUrl: 'https://api.example.com',
  },
})

Access config in handlers

Inside any command or subscription function:

commandBuilder.setCommandFunction(async function (context) {
  const timeout = this.config.apiTimeout
  const url = this.config.externalApiUrl
  // ...
})
Config is immutable after startup

Custom config values are fixed when the service instance is created. If you need runtime-updatable values, use a config store instead.

Validation at startup

Because config uses a Zod schema, invalid values fail fast at instance creation — not midway through a request. This is production-grade: bad config never reaches a customer.

// This will throw a ZodError immediately
const badService = await userServiceV1Service.getInstance(eventBridge, {
  serviceConfig: {
    externalApiUrl: 'not-a-url', // ❌ fails here
  },
})

Related

Read Next
Command

from Core Building Blocks