SolidX
ReferenceExtending SolidXBackend Customization

TypeORM Subscribers

Recommended pattern for TypeORM subscribers in SolidX: publish a background job via PublisherFactory from subscriber hooks and execute business logic in queue subscribers.

TypeORM Subscribers

Recommendation (New Standard)

In SolidX, TypeORM subscriber hooks should always publish a background job and return quickly.

Do not execute heavy business side effects directly inside afterInsert, afterUpdate, etc.

Preferred flow:

  1. TypeORM subscriber hook runs (afterInsert, afterUpdate, ...)
  2. Hook publishes a queue message through PublisherFactory
  3. Queue subscriber processes the actual business logic asynchronously

This publisher-subscriber pair should be your default pattern for subscriber-driven side effects.

This pattern avoids common failure and consistency problems:

  • Avoids long-running work inside DB lifecycle hooks
  • Reduces risk around transaction boundaries and lock duration
  • Gives retries/failure tracking through queue infrastructure
  • Improves reliability for external side effects (email/SMS/webhooks/3rd-party APIs)
  • Keeps entity lifecycle hooks deterministic and lightweight

Anti-Pattern to Avoid

Avoid this in TypeORM subscriber hooks:

  • External API calls
  • File processing/OCR/LLM calls
  • Large cross-entity updates
  • Long computations

Do these in queue subscribers instead.

Implementation Pattern

1) Queue payload and options

import { BrokerType } from "@solidxai/core";

export interface UserChangedPayload {
  userId: number;
  eventType: "created" | "updated";
}

export default {
  name: "userChangedQueueRabbitmq",
  type: BrokerType.RabbitMQ,
  queueName: "user_changed_queue",
  prefetch: 5,
};

2) Publisher

import { Injectable } from "@nestjs/common";
import {
  MqMessageQueueService,
  MqMessageService,
  QueuesModuleOptions,
  RabbitMqPublisher,
} from "@solidxai/core";
import userChangedQueueOptions, { UserChangedPayload } from "./user-changed-queue-options";

@Injectable()
export class UserChangedPublisher extends RabbitMqPublisher<UserChangedPayload> {
  constructor(
    protected readonly mqMessageService: MqMessageService,
    protected readonly mqMessageQueueService: MqMessageQueueService
  ) {
    super(mqMessageService, mqMessageQueueService);
  }

  options(): QueuesModuleOptions {
    return {
      ...userChangedQueueOptions,
    };
  }
}

3) TypeORM subscriber (hook only publishes via PublisherFactory)

import { Injectable } from "@nestjs/common";
import { EventSubscriber, EntitySubscriberInterface, InsertEvent, UpdateEvent } from "typeorm";
import { PublisherFactory } from "@solidxai/core";
import { User } from "../entities/user.entity";
import { UserChangedPayload } from "../background-jobs/user-changed-queue-options";

@Injectable()
@EventSubscriber()
export class UserSubscriber implements EntitySubscriberInterface<User> {
  constructor(
    private readonly publisherFactory: PublisherFactory<UserChangedPayload>
  ) {}

  listenTo() {
    return User;
  }

  async afterInsert(event: InsertEvent<User>) {
    if (!event.entity?.id) return;
    await this.publisherFactory.publish(
      {
        payload: {
          userId: event.entity.id,
          eventType: "created",
        },
        parentEntity: "user",
        parentEntityId: event.entity.id,
      },
      "UserChangedPublisher"
    );
  }

  async afterUpdate(event: UpdateEvent<User>) {
    const id = (event.entity as User | undefined)?.id;
    if (!id) return;
    await this.publisherFactory.publish(
      {
        payload: {
          userId: id,
          eventType: "updated",
        },
        parentEntity: "user",
        parentEntityId: id,
      },
      "UserChangedPublisher"
    );
  }
}

4) Queue subscriber (business logic runs here)

import { Injectable } from "@nestjs/common";
import {
  MqMessageQueueService,
  MqMessageService,
  QueueMessage,
  QueuesModuleOptions,
  RabbitMqSubscriber,
} from "@solidxai/core";
import userChangedQueueOptions, { UserChangedPayload } from "./user-changed-queue-options";

@Injectable()
export class UserChangedJobSubscriber extends RabbitMqSubscriber<UserChangedPayload> {
  constructor(
    readonly mqMessageService: MqMessageService,
    readonly mqMessageQueueService: MqMessageQueueService
  ) {
    super(mqMessageService, mqMessageQueueService);
  }

  options(): QueuesModuleOptions {
    return {
      ...userChangedQueueOptions,
    };
  }

  async subscribe(message: QueueMessage<UserChangedPayload>) {
    const { userId, eventType } = message.payload;
    // Real side effects: notify, sync, call external APIs, etc.
    return { success: true, userId, eventType };
  }
}

Practical Rules

  • Keep TypeORM hook methods minimal and side-effect free except queue publish.
  • Publish via PublisherFactory; do not inject broker-specific publisher classes in subscriber hooks.
  • Prefer logical publisher names (UserChangedPublisher) and let the factory resolve broker-specific implementations.
  • Pass identifiers and minimal context in queue payloads.
  • Do not pass full mutable entity graphs in queue messages.
  • Keep retry/error logic in queue subscriber infrastructure.
  • Use prefetch and worker replicas to scale processing throughput.

Registration

Register TypeORM subscriber + publisher classes + queue subscriber classes as providers in your module. Publishing call sites should depend on PublisherFactory.

Notes

  • You can still use TypeORM hooks for lightweight in-memory data preparation, but not heavy side effects.
  • If in doubt, publish and process asynchronously.

See Also