Docs

Documentation versions (currently viewingVaadin 25.4 (pre-release))

Transactions

Using transactions for atomic signal operations.

Transactions allow grouping multiple signal operations into a single atomic unit. All operations in a transaction either succeed or fail together, ensuring data consistency. A transaction covers a single shared signal and its child signals, as described in Transaction Scope.

Basic Transactions

Use Signal.runInTransaction() to execute multiple signal operations atomically:

Source code
Java
SharedMapSignal<String> person = new SharedMapSignal<>(String.class);

Signal.runInTransaction(() -> {
    // All operations here will be committed atomically
    person.put("firstName", "John");
    person.put("lastName", "Doe");
    person.put("email", "john.doe@example.com");
});

If any operation fails, none of the changes are applied. Observers see either the complete update or nothing at all - never a partial update.

Transaction Scope

A transaction can read and update one shared signal and its child signals: the entries of a SharedListSignal or SharedMapSignal, or the children of a SharedNodeSignal. Each shared signal created with a public constructor is an independent signal tree. It’s committed independently, and in a cluster it may be owned by a different node than another shared signal. Reading or updating a second independent shared signal in the same transaction throws an IllegalStateException:

Source code
Java
SharedValueSignal<String> firstName = new SharedValueSignal<>("");
SharedValueSignal<String> lastName = new SharedValueSignal<>("");

// Throws: firstName and lastName are independent shared signals
Signal.runInTransaction(() -> {
    firstName.set("John");
    lastName.set("Doe");
});

Values that must change together need to live in a single shared signal. Use one of these:

  • entries of one SharedMapSignal or SharedListSignal, when the values have the same type, as in the examples on this page;

  • a SharedValueSignal holding a record, updated as a whole with update(), when the values always change together;

  • a SharedNodeSignal root with a child for each part, when the values have different shapes. See SharedNodeSignal.

Reading a computed signal counts as reading the signals it depends on, so those also need to be within the shared signal that the transaction covers. Local signals, such as ValueSignal and ListSignal, can’t be read or updated inside runInTransaction(). See Local Signals.

Transaction Benefits

Transactions provide several guarantees:

Atomicity

All changes in a transaction succeed or fail together

Consistency

No observer sees partial updates

Isolation

Changes are not visible until the transaction commits

Verification Methods

Transactions support verification methods that ensure certain conditions are met before committing changes.

Verifying Expected Values

The verifyValue() method verifies that a signal has a specific expected value:

Source code
Java
SharedMapSignal<String> order = new SharedMapSignal<>(String.class);
order.put("status", "pending");

Signal.runInTransaction(() -> {
    SharedValueSignal<String> status = order.get().get("status");

    // Transaction fails if current status is not "pending"
    status.verifyValue("pending");

    // Only executed if verification passes
    status.set("processing");
    order.put("processedBy", currentUser);
});

Verifying List Item Position

The verifyPosition() method verifies that an item is at a specific position in a list signal:

Source code
Java
Signal.runInTransaction(() -> {
    SharedValueSignal<Todo> firstItem = todoList.get().get(0);

    // Verify the item we want to update is still first
    todoList.verifyPosition(firstItem, ListPosition.first());

    // Update only if verification passes
    firstItem.set(new Todo("Updated first item", false));
});

Verifying Child Exists

The verifyChild() method on SharedListSignal verifies that a child signal is still part of the list:

Source code
Java
Signal.runInTransaction(() -> {
    SharedValueSignal<Todo> itemSignal = todoList.get().get(0);

    // Verify the child signal still exists in the list before updating
    todoList.verifyChild(itemSignal);

    itemSignal.set(updatedValue);
});

Operation Results

Signal operations return result objects that provide information about the operation and allow chaining.

SignalOperation

The base interface for all signal operations. Operations return a CompletableFuture that resolves when the operation completes:

Source code
Java
SignalOperation<String> op = stringSignal.set("new value");

// Get the result as a CompletableFuture
CompletableFuture<SignalOperation.ResultOrError<String>> result = op.result();

// Check if the operation has completed
if (result.isDone()) {
    // Get the result (blocks if not yet complete)
    SignalOperation.ResultOrError<String> resultOrError = result.get();
    if (resultOrError.successful()) {
        System.out.println("Operation completed successfully");
    }
}

CancelableOperation

Some operations that may retry (like update()) return a CancelableOperation that can be canceled:

Source code
Java
CancelableOperation<Integer> updateOp = counter.update(v -> v + 1);

// If needed, cancel the retry loop
updateOp.cancel();

The update() operation uses a compare-and-set approach. If another change occurs concurrently, the operation retries with the new value. Calling cancel() stops this retry loop.

Note
A call to cancel() may not always be effective, as a succeeding operation might already be on its way to the server.

InsertOperation

List insert operations return an InsertOperation that provides immediate access to the inserted signal:

Source code
Java
SharedListSignal<Todo> todoList = new SharedListSignal<>(Todo.class);

InsertOperation<SharedValueSignal<Todo>> op = todoList.insertLast(new Todo("New task", false));

// Get the signal for the inserted item immediately
SharedValueSignal<Todo> insertedSignal = op.signal();

// You can start working with the signal right away
insertedSignal.update(todo -> new Todo(todo.text(), true));

This is useful when you need to reference the newly inserted item immediately after insertion.

TransactionOperation

When running transactions, Signal.runInTransaction() returns a TransactionOperation that tracks the entire transaction. TransactionOperation extends SignalOperation<Void>, so you can check the transaction status via the inherited result() method:

Source code
Java
TransactionOperation<Void> txOp = Signal.runInTransaction(() -> {
    person.put("firstName", "John");
    person.put("lastName", "Doe");
});

// Check transaction status via the inherited result()
CompletableFuture<SignalOperation.ResultOrError<Void>> result = txOp.result();

if (result.isDone()) {
    System.out.println("Transaction committed successfully");
}

Transactions can also return a value. The returnValue() method provides direct access to the value returned by the transaction callback:

Source code
Java
TransactionOperation<String> txOp = Signal.runInTransaction(() -> {
    statusSignal.verifyValue("pending");
    statusSignal.set("confirmed");
    return "Order confirmed";
});

String returnValue = txOp.returnValue(); // "Order confirmed"

Bypassing Transactions

Effect and computed signal callbacks run inside a read-only transaction to prevent accidental changes. If you need to modify a signal from within an effect (and you’re certain there’s no risk of infinite loops), use runWithoutTransaction():

Source code
Java
Signal.effect(component, () -> {
    String value = sourceSignal.get();

    // WARNING: This might lead to infinite loops.
    // Do this only if absolutely necessary.
    Signal.runWithoutTransaction(() -> {
        derivedSignal.set(transformValue(value));
    });
});
Warning
Using runWithoutTransaction() bypasses safety checks. Only use it when you’re certain the update won’t cause an infinite loop.

Best Practices

Values that should be updated together belong in the same shared signal, so they can be updated in one transaction. See Transaction Scope.

Source code
Java
// Good: Related values in one shared signal, updated in a transaction
Signal.runInTransaction(() -> {
    order.put("status", "shipped");
    order.put("shippedAt", Instant.now().toString());
    order.put("trackingNumber", tracking);
});

// Avoid: Separate updates that others can observe one at a time
order.put("status", "shipped");
order.put("shippedAt", Instant.now().toString());
order.put("trackingNumber", tracking);

Each separate update is committed on its own, so other users can see the order as shipped before it has a tracking number.

Use Verification for Conditional Updates

When updates depend on current state, use verification:

Source code
Java
// Assumes order already has "status" and "assignee" entries
Signal.runInTransaction(() -> {
    SharedValueSignal<String> status = order.get().get("status");
    SharedValueSignal<String> assignee = order.get().get("assignee");

    // Ensure we're updating the expected state
    status.verifyValue("pending");
    assignee.verifyValue(expectedAssignee);

    status.set("confirmed");
    order.put("confirmedBy", currentUser);
});

Prefer update() for Single-Signal Atomic Updates

For atomic updates based on current value, use update() instead of a transaction:

Source code
Java
// Preferred for single-signal updates
counter.update(v -> v + 1);

// Transaction is overkill for single updates
Signal.runInTransaction(() -> {
    counter.set(counter.get() + 1);
});

Keep Transactions Focused

Include only the operations that must succeed or fail together. If any operation in a transaction fails, all of them are rejected, so an unrelated update in the same transaction is lost along with the rest:

Source code
Java
// Good: Only the updates that belong together
Signal.runInTransaction(() -> {
    SharedValueSignal<String> status = order.get().get("status");
    status.verifyValue("pending");
    status.set("confirmed");
    order.put("confirmedAt", Instant.now().toString());
});

// Avoid: An unrelated update that is lost if the verification fails
Signal.runInTransaction(() -> {
    SharedValueSignal<String> status = order.get().get("status");
    status.verifyValue("pending");
    status.set("confirmed");
    order.put("confirmedAt", Instant.now().toString());
    // Doesn't depend on the status, so don't make it conditional
    order.put("lastViewedBy", currentUser);
});