Skip to main content

TransactionParticipant

The TransactionParticipant interface is the building block of every jPOS transaction:

public interface TransactionParticipant extends TransactionConstants {
int prepare(long id, Serializable context);
default void commit(long id, Serializable context) { }
default void abort(long id, Serializable context) { }
}

The two-phase protocol

Every transaction runs in two phases:

If any participant returns ABORTED during prepare, the TM stops the prepare chain and calls abort() on every participant that already joined, in reverse order:

Return codes

prepare() returns an int built from TransactionConstants:

ConstantValueMeaning
PREPARED1This participant is ready. Continue the prepare chain.
ABORTED0Abort the transaction. Call abort() on all joined participants.
RETRY2Put the context back on the retry queue and try again later.
PAUSE4Suspend the transaction (async continuation).

These can be combined with modifier flags:

FlagValueMeaning
NO_JOIN0x40Don't call commit() or abort() on this participant.
READONLY0x80The context was not modified. No need to persist a snapshot.
FAILREADONLY|NO_JOINShortcut for ABORTED | READONLY | NO_JOIN.

READONLY is a performance optimisation: without it, the TM persists a snapshot of the context after every prepare so it can recover mid-transaction after a crash.

HandleNMM — the tutorial participant

public class HandleNMM implements TransactionParticipant {

private static final Set<String> APPROVED = Set.of("001", "002", "301");

@Override
public int prepare(long id, Serializable context) {
Context ctx = (Context) context;
try {
ISOMsg req = ctx.get(REQUEST.toString());
ISOMsg resp = (ISOMsg) req.clone();
resp.setResponseMTI();

String f70 = req.getString(70);
if (APPROVED.contains(f70)) {
resp.set(39, "0000");
ctx.put(RESPONSE.toString(), resp);
ctx.log("approve mti=" + resp.getMTI() + " f70=" + f70);
return PREPARED | READONLY;
} else {
resp.set(39, "9100");
ctx.put(RESPONSE.toString(), resp);
ctx.log("decline f70=" + f70 + " (unknown function code)");
return ABORTED | READONLY;
}
} catch (ISOException e) {
ctx.log(e);
return ABORTED | READONLY;
}
}
}

Key decisions:

READONLYHandleNMM always returns READONLY because it only reads the request and writes the response into the transient context map. The TM does not need to persist a snapshot after it runs.

ABORTED path still sets RESPONSE — when f70 is not in the approved set, the participant puts a 9100 response into the context before returning ABORTED. This is deliberate: SendResponse, as an AbortParticipant, will still send whatever is in RESPONSE even on the abort path. Without this, the client would receive no response at all.

No commit() or abort() overrideHandleNMM's work is done in prepare(). Because it returns READONLY, the TM doesn't need to call anything on commit or abort.

The participant chain as a pipeline

Participants are called in declaration order during prepare. This makes the chain a readable processing pipeline in XML:

<participant class="org.jpos.tutorial.DecodeMessage"    realm="decode"   />
<participant class="org.jpos.tutorial.ValidateFields" realm="validate" />
<participant class="org.jpos.tutorial.AuthorizeRequest" realm="authorize"/>
<participant class="org.jpos.transaction.participant.SendResponse" realm="send"/>

Each participant reads what earlier ones put in the context, adds its own contribution, and signals PREPARED or ABORTED. If ValidateFields aborts, DecodeMessage's abort() is called (cleanup), and SendResponse.prepareForAbort() fires so the client still gets a response.

RETRY

Returning RETRY from prepare() puts the context on a retry queue (persistent space). The TM retries it after retry-interval milliseconds, up to retry-timeout. Useful for transient failures (network timeouts, database locks) that may resolve on their own. Not used in this tutorial but important in production.

PAUSE

Returning PAUSE suspends the transaction pending an async callback. The worker thread returns to the pool. The transaction resumes when ctx.resume(result) is called from another thread. Used for async authorisation, PIN entry on external terminals, etc.