SendDart in Java Applications: Respect Transactions and Executor Budgets
Plan a Java email adapter with typed recovery, transaction-aware intent and bounded execution rather than untracked asynchronous side effects.
TL;DR
- Treat transaction-bound listeners as non-guarantees and design the integration around a durable business notification record that stores intent and per-attempt evidence for recovery.
- Keep network work out of long database transactions: record intent with the business change, commit, then run a durable worker; budget each attempt across DB, rendering, HTTP, SDK retries and persistence.
- Treat tests and metrics as the success check: exercise the transaction-to-worker boundary and verify counts of intended notifications versus external attempts, not merely that a future completed.
A transaction listener is not a delivery guarantee
Java applications often have mature transaction and event abstractions. Those abstractions help organize email work, but an in-process event still has a lifetime tied to the application. If a process stops after a database commit and before the external send, a notification can be lost unless its intent is retained durably.
Design the integration around a business notification record. It identifies the event, recipient, content version and provider operation identity. A worker processes that record and stores evidence of each attempt. Framework events can coordinate the work without becoming its only durable source.
For Spring applications, transaction-bound event listeners provide documented phase behavior. Understand that phase and the active transaction context before using a listener to perform external side effects. Spring transaction-bound events.
Related reading: Scheduled Email Delivery: Store the Time, Content and Cancellation State.
Review the SDK in the actual Java runtime
SendDart's Java SDK documents a Java 11-compatible artifact and uses Java's HTTP client. It exposes request builders, a response wrapper and SendDartException for documented API failures. Verify the package and runtime in your deployed service, including any container or modular-runtime constraints. SendDart Java SDK.
Keep the provider wrapper behind a small application interface. Business services should request a named notification rather than construct arbitrary provider payloads throughout the codebase. This keeps sender policy, error translation and recovery consistent.
Read structured exception fields instead of matching message strings. Retain status, machine-readable name and relevant progress evidence. A typed exception is helpful only if the adapter preserves distinctions that the worker needs to choose a safe next action.
Related reading: Best Email API: Choose With a Production Acceptance Test.
Keep network work out of long database transactions
Holding a database transaction open while waiting for an email service can increase lock duration and complicate rollback semantics. More importantly, the external send cannot generally be rolled back with the database transaction.
Record notification intent with the business change, then perform the provider operation after commit through a durable worker. A transaction event can notify a dispatcher, while the retained record remains recoverable if that notification is lost.
Use a uniqueness constraint or equivalent concurrency-safe rule for business event and message purpose. Two threads handling duplicate upstream events should not create two independent notifications simply because both passed a pre-insert lookup.
Bound executor and client behavior
An executor pool controls how many tasks run, but an in-memory queue is not automatically durable. Define capacity, rejection behavior and shutdown handling. An unbounded executor queue can hide overload until memory or latency becomes unacceptable.
Budget each attempt across database access, rendering, HTTP calls, SDK retries and result persistence. Configure the client according to the documented SDK options and your worker deadline. Do not add a second generic retry layer without accounting for the first.
If execution is interrupted, preserve the operation's last known stage. A canceled future or interrupted thread does not prove the remote service did nothing. The worker may need to retrieve or reconcile the original message before another send is permitted.
Make rendering reproducible
Choose an immutable template version and input snapshot for each intended notification. A retry after deployment should not silently change content under the same operation identity. If the business requires a corrected message, create a new authorized notification with its own reason.
Test serialization and character handling with representative content: non-ASCII subjects, long names, HTML escaping and a plain-text alternative. Avoid concatenating untrusted values into headers or markup without the appropriate validation and escaping.
Attachments need a similar version boundary. If a worker fetches a file by URL, ensure the URL identifies the intended document and remains accessible for the required processing window. Do not accidentally attach a newly overwritten file during recovery.
Preserve idempotency and partial progress
Supported SendDart send operations can carry a stable operation key. Store the key before submission and retain the original payload. Reusing a key with changed content is not a safe correction strategy, and generating a new key to bypass a conflict can create duplicates.
Batch operations need item-level recovery evidence. Keep confirmed, attempted and never-attempted work distinct when the provider reports them. A single failed boolean cannot support a safe decision to resend the entire collection.
Expose uncertain states to operations. A Java exception hierarchy should not force every outcome into success or failure if the underlying distributed operation is ambiguous. The notification record is the place to preserve that uncertainty across process restarts.
Integrate event evidence under the correct scope
Webhook handling should authenticate the original request and persist a unique observation before slow processing. Use stored provider message ownership to select the application record and tenant. Do not authorize a mutation solely from an arbitrary identifier inside the payload.
Handle an event that arrives before the sending worker commits its response. Retain a controlled unmatched observation and reconcile it later. Duplicate callbacks should not repeat downstream alerts or suppression mutations.
Keep private bodies, tokens and credentials out of general exception logs. A stable notification ID, provider ID and event timeline can support most investigations without exposing message content broadly.
Test the transaction-to-worker boundary
Exercise rollback, lost dispatch signaling, concurrent duplicate events and executor shutdown during a send. Verify the number of intended notifications and the number of external attempts, not merely that a future eventually completed.
Add a controlled integration canary for the actual SDK, credential and sender setup. Keep it separate from deterministic tests that simulate rejection or interruption.
The result is a Java integration that uses framework abstractions without depending on their process lifetime for durability. Transactions retain intent, workers control execution, and structured provider evidence guides recovery.