Docs

Documentation versions (currently viewingVaadin 25.1 (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.

Note
Preview Feature

This is a preview version of Signals. You need to enable it with the feature flag com.vaadin.experimental.flowFullstackSignals. Preview versions may lack some planned features, and breaking changes may be introduced in any Vaadin version. We encourage you to try it out and provide feedback to help us improve it.

Basic Transactions

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

Source code
Java
Signal.runInTransaction(() -> {
    // All operations here will be committed atomically
    firstNameSignal.value("John");
    lastNameSignal.value("Doe");
    ageSignal.value(30);
});

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 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
Signal.runInTransaction(() -> {
    // Transaction fails if current status is not "pending"
    statusSignal.verifyValue("pending");

    // Only executed if verification passes
    statusSignal.value("processing");
    processedBySignal.value(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.value().get(0);

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

    // Update only if verification passes
    firstItem.value(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.value().get(0);

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

    itemSignal.value(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.value("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<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, you can get a TransactionOperation that tracks the entire transaction. The returnValue() method returns a SignalOperation that can be used to check the transaction result:

Source code
Java
TransactionOperation txOp = Signal.runInTransaction(() -> {
    firstNameSignal.value("John");
    lastNameSignal.value("Doe");
});

// Get the underlying SignalOperation to check transaction status
SignalOperation<?> signalOp = txOp.returnValue();
CompletableFuture<?> result = signalOp.result();

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

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
ComponentEffect.effect(component, () -> {
    String value = sourceSignal.value();

    // WARNING: This might lead to infinite loops.
    // Do this only if absolutely necessary.
    Signal.runWithoutTransaction(() -> {
        derivedSignal.value(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

When multiple signals should be updated together, use a transaction:

Source code
Java
// Good: Related updates in a transaction
Signal.runInTransaction(() -> {
    orderStatusSignal.value("shipped");
    shippedAtSignal.value(Instant.now());
    trackingNumberSignal.value(tracking);
});

// Avoid: Separate updates that might leave inconsistent state
orderStatusSignal.value("shipped");
shippedAtSignal.value(Instant.now()); // What if this fails?
trackingNumberSignal.value(tracking);

Use Verification for Conditional Updates

When updates depend on current state, use verification:

Source code
Java
Signal.runInTransaction(() -> {
    // Ensure we're updating the expected state
    statusSignal.verifyValue("pending");
    quantitySignal.verifyValue(expectedQuantity);

    statusSignal.value("confirmed");
    quantitySignal.value(newQuantity);
});

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.value(counter.value() + 1);
});

Keep Transactions Small

Transactions hold resources while executing. Keep them focused on the minimal set of related operations:

Source code
Java
// Good: Small, focused transaction
Signal.runInTransaction(() -> {
    userSignal.value(updatedUser);
    lastModifiedSignal.value(Instant.now());
});

// Avoid: Large transactions with unrelated operations
Signal.runInTransaction(() -> {
    userSignal.value(updatedUser);
    lastModifiedSignal.value(Instant.now());
    // Don't include unrelated updates
    analyticsCounterSignal.incrementBy(1);
    logSignal.value(logMessage);
});