Skip to content

The DomainContextProvider — Creating PDOs and Operations in Context

Overview and Motivation

Every piece of domain logic in a Tentackle application runs somewhere: in a particular database session, on behalf of a particular tenant, possibly inside a named task or aggregate. That "somewhere" is the DomainContext — the object that ties a PersistentDomainObject (PDO) or Operation to its session, its aggregate root context, and its tenant.

The problem the DomainContextProvider solves is propagation. When domain code on one entity needs to reach another entity — load related rows, create a new PDO, invoke a service — that new object must end up in the same context: the same session, the same tenant, the same transaction. Passing a DomainContext argument through every call would be noise, and forgetting it would silently break tenant scoping or transactional integrity.

DomainContextProvider is the small interface that makes the current context ambient for any object that has one. It exposes that context and adds the convenience factory methods on(...) and op(...) that create new PDOs and Operations within it, so domain logic stays inside its context without ever naming it.

public interface DomainContextProvider {
  DomainContext getDomainContext();

  default <T extends PersistentDomainObject<T>> T on(Class<T> clazz) { ... }   // create a PDO
  default <T extends Operation<T>>             T op(Class<T> clazz) { ... }    // create an Operation
  // + named-context overloads and isWithinContext(...) checks
}

Who Provides a Context

DomainContextProvider is not something the application implements by hand — it is inherited by the abstractions you already work with, so the methods are simply there:

Provider Why it has a context
DomainObject (the domain half of every PDO) extends ... DomainContextProvider, SessionProvider — so domain code on any entity can reach others.
DomainOperation (the domain half of every Operation) extends ... DomainContextProvider, SessionProvider — operations create PDOs in their own context too.
Application extends SessionProvider, DomainContextProvider — the application's entry context, the root of propagation.
UI / RDC components (PdoEditor, GuiProvider, PdoFinder, …) carry the context of what the user is currently editing.

Because getDomainContext() is the single abstract method, every one of these gains on(...), op(...), and isWithinContext(...) for free as default methods.


Obtaining the First Context

Propagation has to start somewhere. The very first DomainContext is built from an open Session — typically once, when the application logs in:

DomainContext context = Pdo.createDomainContext(session);

Pdo.createDomainContext(...) wraps the session in a DefaultDomainContext (itself just new DefaultDomainContext(session)). From that root context onward, domain code never calls createDomainContext again — it uses on(...)/op(...), which clone or reuse the context it is already in. In a typical application the Application singleton holds this root context and hands it out via getDomainContext().

A DomainContext is a SessionHolder: it carries the session in which its objects live, and therefore the transaction. Two objects sharing a context share a session; that is what makes "in the same context" mean "in the same transaction".


Creating a PDO Within a Context

There are two equivalent spellings, and one is just sugar for the other.

The explicit form — Pdo.create

Invoice invoice = Pdo.create(Invoice.class, context);
List<Invoice> open = invoice.selectByCustomerId(customerId);

Pdo.create(Class, DomainContext) delegates to the PdoFactory SPI, which assembles the two-delegate proxy (persistence half + domain half) and attaches the given context. Use this form at the boundary, where you hold a DomainContext directly but are not yet inside a provider.

The ambient form — on(...)

Inside any DomainContextProvider — that is, inside any domain implementation — write:

Invoice invoice = on(Invoice.class);              // same context as 'this'

which is exactly:

Invoice invoice = Pdo.create(Invoice.class, getDomainContext());

on(...) is "object new": it reads cleanly and, crucially, guarantees the new PDO inherits this object's session, tenant, and context. This is how domain logic reaches other entities while staying inside the current context. A worked example from pdo.md:

@Override
public IngotCategory determineCategory() {
  for (IngotCategory category : on(IngotCategory.class).selectAllCached()) {
    if (category.isMatching(me())) {
      return category;
    }
  }
  return null;
}

Here IngotDomainImpl (the domain half of Ingot) creates an IngotCategory PDO in its own context, queries with it, and compares against me() (the whole Ingot proxy) — all without ever mentioning a session or a context object.


Creating an Operation Within a Context

An Operation is Tentackle's home for behavior that is not tied to one entity — sending mail, running a batch, dispatching a command. It reuses the same PDO machinery (proxy, context, SPI injection, TRIP remoting) and is created the same way, through its own factory:

// explicit
Pdo.createOperation(MailOperation.class, context)
   .send("plsbl", recipient, "test", "this is a test from PLSBL");

// ambient, inside a provider
op(MailOperation.class).send(...);

op(...) mirrors on(...): it calls OperationFactory.create(clazz, getDomainContext()), so the operation runs in the caller's session, transaction, and tenant. Because an operation carries a context just like a PDO, it can itself call on(...)/op(...) to drive entities and further operations — all in the one context.


Named Sub-Contexts

Both on and op have an overload that takes a context name:

Ingot ingot = on(Ingot.class, "edit");        // a fresh named sub-context
MailOperation mail = op(MailOperation.class, "import");

These clone the current context into a named sub-context (getDomainContext().clone(contextName)) before creating the object. A named context describes a distinct part of the application — a module, task, or purpose — and serves two roles in domain logic:

  • Method gating. Methods annotated with @WithinContext / @NotWithinContext are allowed or rejected by an interceptor that checks the live context name, so certain operations are only callable inside (or outside) a given task context.
  • Lock isolation. A PDO locked in one named context cannot be locked, persisted, or unlocked by the same user from a different named context — preventing a user from stepping on their own edit session in another module.

The provider also exposes the matching predicates — isWithinContext(String name) and isWithinContext(long contextId, int contextClassId) — which domain code can consult directly, and which the interceptors above are built on.


Why the Context Is Central to Domain Logic

The provider's small surface (getDomainContext + on/op) is the seam through which the framework's core guarantees propagate automatically. Every object a piece of domain logic creates with on(...)/op(...) inherits, by construction:

  1. The session and transaction. A context is a SessionHolder; staying in-context keeps work in one transaction, so an aggregate and everything it touches commit or roll back together.

  2. The aggregate root context. Because each root entity maintains its own root context, creating and persisting components in-context is what lets the framework enforce DDD aggregate boundaries — and what backs the rootId/rootClassId reasoning used by security and immutability.

  3. Tenant / logical-data-space scoping. An application-specific context (built via a DomainContextFactory) can carry tenant ids; a selectAll() then returns only that tenant's rows (logical data spaces). on(...) propagates that scoping silently, so cross-tenant leaks can't arise from forgetting to pass it.

  4. Authorization. The SecurityManager evaluates permissions within a domain context (security.md); objects created in-context are checked under the right grantee and scope, and DomainContext.assertPermissions() is invoked on (re)attachment.

The pattern, then, is: obtain one context at the application boundary, and from there let domain logic create everything it needs with on(...) and op(...). The context is never threaded through signatures and never re-derived — it simply flows with the work, which is exactly why every PDO and operation ends up in the right session, tenant, aggregate, and permission scope.


Summary

Member Purpose
getDomainContext() The single abstract method — the ambient context of this object.
on(Class<T>) Create a PDO of another type in this context (sugar for Pdo.create(clazz, getDomainContext())).
on(Class<T>, String) Same, but in a fresh named sub-context.
op(Class<T>) Create an Operation in this context.
op(Class<T>, String) Same, but in a named sub-context.
isWithinContext(...) Test the current named/object context — the basis of @WithinContext gating.

See also