Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

The Transactional Orchestrator

Between the world and the domain sits a layer that owns how the operation runs without owning what the operation means. @ApplicationService from @frenchexdev/ddd-application-service reifies that layer: load the aggregate, call its method, save the result, dispatch its events, commit the transaction. No domain logic of its own — that lives in the aggregate.


What @ApplicationService Reifies

The pattern's discipline is thin. A good application service method is fifteen lines: load the aggregate, validate the command (often via the aggregate's Result-returning method), commit the result, dispatch events, return success. If the method grows past fifty lines, the suspicion is that domain logic has crept in — pull it back into the aggregate.

The two responsibilities of the application service are orchestration (what to do in what order) and transactional alignment (when to commit, when to dispatch, when to rollback). Domain logic — the rules about what makes a valid subscription, what a cancellation means — belongs to the aggregate. The application service decides when the rules run; the aggregate decides what they are.

The decorator's transactional flag defaults to true because that is the common case: every method on an application service runs inside a single transaction opened by the caller (typically the command handler or the route handler). Setting it to false is the opt-out for read-side services where no transaction is needed.


The Runtime: ddd-application-service

decorator.ts declares the surface. Required name: string, optional transactional: boolean (default true). The decorator stamps __applicationService and __applicationServiceMetadata.

In our invented Subscription context:

import { ApplicationService } from '@frenchexdev/ddd-application-service';
import { ok, err, type Result } from '@frenchexdev/ddd-core';
import type { SubscriptionRepositoryPort } from '../ports/subscription-repository.port.js';
import type { EventBus } from '@frenchexdev/ddd-event-bus';

@ApplicationService({ name: 'SubscriptionService', transactional: true })
export class SubscriptionService {
  constructor(
    private readonly subscriptions: SubscriptionRepositoryPort,
    private readonly bus:           EventBus,
  ) {}

  async cancelSubscription(cmd: CancelSubscriptionCommand): Promise<Result<void, CancelFailure>> {
    const sub = await this.subscriptions.load(cmd.subscriptionId);
    if (!sub) return err({ kind: 'NotFound', id: cmd.subscriptionId });

    const cancellation = sub.cancel(cmd.reason);
    if (!cancellation.ok) return err(cancellation.error);

    await this.subscriptions.save(sub);
    await this.bus.publish(cancellation.value);
    return ok(undefined);
  }
}

Fourteen lines including the imports. The method does exactly what it says — load, call, save, dispatch — and nothing else. The cancellation's rules (cannot cancel an already-cancelled subscription) live in sub.cancel(reason) on the aggregate. The cancellation's transactional shape lives here.


The Codegen and Analyzer

Spec-first triplet. The analyzer enforces a Service suffix at info severity, single-per-file, and a future cross-pattern rule will check that the service does not import domain logic except through aggregates and repositories — the thin-layer discipline made checkable.

The codegen emits a service registry, useful for the application's bootstrap to wire every service with its dependencies.


Back to the series index.

⬇ Download