Skip to content

Feature Slices

As your application grows, defining all job types and processors in a single file becomes unwieldy. Feature slices let you split them by domain — each slice owns its type definitions and processor handlers, composed together at the application level.

A slice consists of two files: definitions and processors.

  • Directorysrc/
    • slice-orders-definitions.ts
    • slice-orders-processors.ts
    • slice-notifications-definitions.ts
    • slice-notifications-processors.ts
    • client.ts
    • index.ts

Definitions declare the job types for a feature:

slice-orders-definitions.ts
import { defineJobTypeRegistry } from "queuert";
export const orderJobTypeRegistry = defineJobTypeRegistry<{
"orders.create": { entry: true; input: { userId: string }; output: { orderId: string } };
"orders.fulfill": { input: { orderId: string }; output: { fulfilled: boolean } };
}>();

Processors implement the handlers, typed against the slice’s definitions:

slice-orders-processors.ts
import { createJobTypeProcessorRegistry } from "queuert";
import { client } from "./client.js";
import { orderJobTypeRegistry } from "./slice-orders-definitions.js";
export const orderJobTypeProcessorRegistry = createJobTypeProcessorRegistry({
client,
jobTypeRegistry: orderJobTypeRegistry,
processors: {
"orders.create": {
attemptHandler: async ({ job, complete }) =>
complete(async ({ continueWith }) =>
continueWith({ typeName: "orders.fulfill", input: { orderId: "123" } }),
),
},
"orders.fulfill": {
attemptHandler: async ({ job, complete }) => complete(async () => ({ fulfilled: true })),
},
},
});

createJobTypeProcessorRegistry type-checks each handler against the slice’s own definitions, then returns a JobTypeProcessorRegistry that carries the slice’s type definitions via phantom symbol properties. This enables lightweight compatibility checks when passed to createInProcessWorker or mergeJobTypeProcessorRegistries.

At the application level, merge registries and processors from all slices:

import {
createClient,
createInProcessWorker,
mergeJobTypeRegistries,
mergeJobTypeProcessorRegistries,
} from "queuert";
import { orderJobTypeRegistry } from "./slice-orders-definitions.js";
import { orderJobTypeProcessorRegistry } from "./slice-orders-processors.js";
import { notificationJobTypeRegistry } from "./slice-notifications-definitions.js";
import { notificationJobTypeProcessorRegistry } from "./slice-notifications-processors.js";
const mergedJobTypeRegistry = mergeJobTypeRegistries({ slices: [orderJobTypeRegistry, notificationJobTypeRegistry] });
const client = await createClient({ stateAdapter, notifyAdapter, jobTypeRegistry: mergedJobTypeRegistry });
const worker = await createInProcessWorker({
client,
jobTypeProcessorRegistry: mergeJobTypeProcessorRegistries({
slices: [orderJobTypeProcessorRegistry, notificationJobTypeProcessorRegistry],
}),
});

mergeJobTypeRegistries detects overlapping keys at compile time and at runtime:

  • Compile-time — overlapping type names produce a TypeScript error
  • Runtime — validated registries with overlapping getTypeNames() throw DuplicateJobTypeError

mergeJobTypeProcessorRegistries detects overlapping processor keys at runtime, throwing DuplicateJobTypeError.

When a slice needs to reference job types from another slice — for example, declaring a blocker from the notifications domain — use the optional TExternal type parameter on defineJobTypeRegistry:

slice-orders-definitions.ts
import { type JobTypeRegistryDefinitions, defineJobTypeRegistry } from "queuert";
import { type notificationJobTypeRegistry } from "./slice-notifications-definitions.js";
export const orderJobTypeRegistry = defineJobTypeRegistry<
{
"orders.place": {
entry: true;
input: { userId: string };
continueWith: { typeName: "orders.confirm" };
};
"orders.confirm": {
input: { orderId: string };
output: { confirmed: boolean };
blockers: [{ typeName: "notifications.send" }];
};
},
// External types — available for blocker reference validation, not owned by this slice
JobTypeRegistryDefinitions<typeof notificationJobTypeRegistry>
>();
  • T (first parameter) = owned definitions — these become the registry’s phantom type
  • TExternal (second parameter) = read-only reference context, defaults to Record<never, never>
  • blockers validates against entry types in T & TExternal
  • The registry’s phantom type remains T only — TExternal types are not included

This eliminates the need for “workflow slices” that duplicate type definitions just to make blocker references type-check. After merging with mergeJobTypeRegistries, all references resolve against the full set of definitions.

When writing processors for a slice with external references, createJobTypeProcessorRegistry automatically extracts both owned and external definitions from the registry:

slice-orders-processors.ts
import { createJobTypeProcessorRegistry } from "queuert";
import { client } from "./client.js";
import { orderJobTypeRegistry } from "./slice-orders-definitions.js";
const orderJobTypeProcessorRegistry = createJobTypeProcessorRegistry({
client,
jobTypeRegistry: orderJobTypeRegistry,
processors: {
// handlers have full type inference for continueWith, blockers, etc.
},
});
orders.create-order
orders.fulfill-order
notifications.send-notification