Overview
ACID transactions represent a set of four properties -- Atomicity, Consistency, Isolation, and Durability -- that guarantee reliable processing of database operations. These properties ensure that even under concurrent access, system failures, or network partitions, data remains in a valid and predictable state. First formalized by Andreas Reuter and Theo Harder in 1983, these guarantees became the cornerstone of relational database management systems and remain critical in modern distributed architectures.
In the Prismatic Platform, ACID transactions are the backbone of all persistent data operations, enforced through PostgreSQL and the Ecto library across the entire 115-application umbrella ecosystem. Every data mutation flows through Ecto changesets and transactions, ensuring data integrity at every layer.
Definition and Properties
Atomicity
Atomicity ensures that a transaction is treated as a single, indivisible unit of work. Either all operations within the transaction succeed, or none of them take effect. There is no partial application of changes. This is implemented through transaction logs and rollback mechanisms -- when a transaction begins, the database records all changes in a write-ahead log before applying them to the actual data pages. If the transaction fails at any point, the database uses the log to undo all changes.
Consistency
Consistency guarantees that a transaction brings the database from one valid state to another valid state. All defined rules, constraints, cascades, and triggers must be satisfied before a transaction commits. This includes CHECK constraints, UNIQUE constraints, FOREIGN KEY constraints, and any application-level invariants enforced through Ecto changesets.
Isolation
Isolation ensures that concurrent transactions execute as if they were serial -- one after another. The intermediate state of a transaction is invisible to other concurrent transactions, preventing phenomena like dirty reads, non-repeatable reads, and phantom reads. PostgreSQL implements isolation through Multi-Version Concurrency Control (MVCC) rather than locking, allowing readers and writers to operate concurrently without blocking each other.
Durability
Durability guarantees that once a transaction has been committed, its effects persist permanently, even in the event of system crashes, power failures, or hardware malfunctions. This is achieved through PostgreSQL's Write-Ahead Logging (WAL) and checkpoint mechanisms.
Together, these four properties form the theoretical foundation upon which reliable data systems are built. In practice, systems often make calculated tradeoffs between strict ACID compliance and performance or availability, particularly in distributed environments where the CAP theorem imposes fundamental constraints.
Historical Context
The ACID acronym was coined in a 1983 paper by Andreas Reuter and Theo Harder, formalizing properties that database systems had been implementing since the 1970s. Jim Gray's pioneering work on transaction processing in the late 1970s established the theoretical foundations, and his concept of transaction isolation levels later became part of the SQL standard (SQL-92).
The evolution of ACID transactions has been marked by a tension between correctness and performance. Early systems used strict two-phase locking (2PL), which guaranteed serializability but created significant contention. The development of MVCC by Michael Stonebraker and others in the 1980s (first implemented in Postgres, the predecessor to PostgreSQL) provided a breakthrough -- concurrent reads and writes without mutual blocking.
The rise of distributed systems in the 2000s brought new challenges. Eric Brewer's CAP theorem (2000) demonstrated that distributed systems cannot simultaneously guarantee consistency, availability, and partition tolerance. This led to the BASE (Basically Available, Soft state, Eventually consistent) model and NoSQL databases that traded ACID guarantees for horizontal scalability. However, the pendulum has swung back: modern distributed databases like CockroachDB, YugabyteDB, and Google Spanner prove that ACID can be achieved at scale, albeit with latency tradeoffs.
Technical Deep Dive
Ecto.Multi for Atomic Multi-Step Operations
The Prismatic Platform uses Ecto.Multi extensively for complex business operations that span multiple database tables:
alias Ecto.Multi
multi =
Multi.new()
|> Multi.insert(:user, User.changeset(%User{}, %{name: "Alice", email: "alice@example.com"}))
|> Multi.insert(:profile, fn %{user: user} ->
Profile.changeset(%Profile{}, %{user_id: user.id, bio: "New user"})
end)
|> Multi.update(:audit, fn %{user: user} ->
AuditLog.changeset(%AuditLog{}, %{
action: "user_created",
entity_id: user.id,
entity_type: "user"
})
end)
case Repo.transaction(multi) do
{:ok, %{user: user, profile: profile, audit: _audit}} ->
{:ok, user}
{:error, failed_operation, changeset, _changes_so_far} ->
# ALL operations rolled back automatically
{:error, {failed_operation, changeset}}
endIsolation Levels
PostgreSQL supports four standard isolation levels, each providing different tradeoffs between correctness and performance:
| Level | Dirty Reads | Non-Repeatable Reads | Phantom Reads | Serialization Anomalies |
|---|---|---|---|---|
| Read Uncommitted | Not possible* | Possible | Possible | Possible |
| Read Committed | Not possible | Possible | Possible | Possible |
| Repeatable Read | Not possible | Not possible | Not possible** | Possible |
| Serializable | Not possible | Not possible | Not possible | Not possible |
*PostgreSQL treats Read Uncommitted as Read Committed. **PostgreSQL's Repeatable Read also prevents phantom reads via snapshot isolation.
# Setting isolation level in Ecto transactions
Repo.transaction(
fn ->
accounts = Repo.all(from a in Account, where: a.balance > 0, lock: "FOR UPDATE")
Enum.each(accounts, fn account ->
new_balance = account.balance + calculate_interest(account)
Repo.update!(Account.changeset(account, %{balance: new_balance}))
end)
end,
isolation_level: :serializable
)Transaction Retry with Exponential Backoff
Under Serializable isolation, the database may abort transactions to prevent anomalies. The platform implements retry logic:
defmodule Prismatic.TransactionManager do
@moduledoc """
Manages complex multi-step transactions with proper isolation
and conflict resolution across the Prismatic Platform.
"""
alias Ecto.Multi
alias Prismatic.Repo
@spec execute_with_retry(Ecto.Multi.t(), keyword()) ::
{:ok, map()} | {:error, term()}
def execute_with_retry(multi, opts \\ []) do
max_retries = Keyword.get(opts, :max_retries, 3)
isolation = Keyword.get(opts, :isolation_level, :read_committed)
do_execute(multi, isolation, max_retries, 0)
end
defp do_execute(_multi, _isolation, max_retries, attempt)
when attempt >= max_retries do
{:error, :max_retries_exceeded}
end
defp do_execute(multi, isolation, max_retries, attempt) do
case Repo.transaction(multi, isolation_level: isolation) do
{:ok, results} ->
{:ok, results}
{:error, :serialization_failure, _, _} ->
backoff = :math.pow(2, attempt) |> round() |> :timer.seconds()
Process.sleep(backoff)
do_execute(multi, isolation, max_retries, attempt + 1)
{:error, operation, changeset, _} ->
{:error, {operation, changeset}}
end
end
endWrite-Ahead Logging (WAL) and Durability Configuration
# Production configuration for durability
config :prismatic, Prismatic.Repo,
pool_size: 20,
queue_target: 50,
queue_interval: 1000,
parameters: [
synchronous_commit: "on",
wal_level: "replica",
checkpoint_timeout: "5min"
]Security Rating Calculation with ACID Guarantees
defmodule Prismatic.SecurityRating.Calculator do
@moduledoc """
Calculates security ratings with full ACID guarantees.
All rating updates are atomic - partial updates never occur.
"""
alias Ecto.Multi
def recalculate_rating(domain_id) do
Multi.new()
|> Multi.run(:current_rating, fn _repo, _changes ->
fetch_current_rating(domain_id)
end)
|> Multi.run(:scan_results, fn _repo, _changes ->
aggregate_scan_results(domain_id)
end)
|> Multi.run(:new_rating, fn _repo, %{scan_results: results} ->
compute_rating(results)
end)
|> Multi.insert(:rating_record, fn %{new_rating: rating} ->
SecurityRating.changeset(%SecurityRating{}, %{
domain_id: domain_id,
score: rating.score,
grade: rating.grade,
calculated_at: DateTime.utc_now()
})
end)
|> Multi.run(:notify, fn _repo, %{rating_record: record} ->
broadcast_rating_change(record)
end)
|> Repo.transaction()
end
end
ACID vs BASE
The BASE (Basically Available, Soft state, Eventually consistent) model represents the opposite end of the consistency spectrum:
| Property | ACID | BASE |
|---|---|---|
| Consistency | Strong, immediate | Eventual |
| Availability | May sacrifice under contention | Prioritized |
| Partition tolerance | Limited in distributed settings | Built-in |
| Use case | Financial, compliance, security | Social media, caching, analytics |
| Prismatic usage | Primary data store (PostgreSQL) | Cache layers (ETS, Redis) |
Distributed Transaction Approaches
| Approach | Pros | Cons | Prismatic Usage |
|---|---|---|---|
| Two-Phase Commit (2PC) | Strong consistency | Blocking, coordinator failure | Not used (single DB) |
| Saga Pattern | Non-blocking, compensatable | Complex, eventual consistency | Cross-service workflows |
| Event Sourcing | Full audit trail, replayable | Storage overhead, complexity | OSINT pipelines |
| Outbox Pattern | Reliable messaging + ACID | Additional table, polling | Event publishing |
Best Practices
-
Use Ecto.Multi for multi-step operations. Never perform multiple independent
Repo.insert/updatecalls outside a transaction when they must succeed or fail together. -
Choose the right isolation level. Default to Read Committed for most operations. Use Serializable only when absolute correctness under high concurrency is required.
-
Keep transactions short. Long-running transactions hold locks and can cause contention. Move non-database work (HTTP calls, file I/O) outside the transaction boundary.
-
Use advisory locks for application-level coordination. PostgreSQL advisory locks provide lightweight locking without row-level contention.
-
Handle serialization failures with retry logic. Under Serializable isolation, always implement retry with exponential backoff.
-
Validate data in changesets before the transaction. Catching validation errors before entering a transaction reduces unnecessary rollbacks and connection usage.
-
Monitor transaction duration and deadlocks. Use PostgreSQL's
pg_stat_activityandlog_lock_waitsto detect long-running transactions and deadlock patterns. -
Prefer optimistic concurrency control. Use Ecto's
optimistic_lock/1for low-contention updates rather than pessimisticFOR UPDATElocks.
Common Pitfalls
-
Transaction scope creep. Placing external API calls, email sending, or file operations inside a transaction. These side effects cannot be rolled back and can cause transactions to hold connections indefinitely.
-
N+1 transactions. Executing individual transactions in a loop instead of batching operations into a single
Ecto.Multipipeline. -
Ignoring isolation level defaults. PostgreSQL defaults to Read Committed, which allows non-repeatable reads. This can cause subtle bugs in read-modify-write patterns.
-
Deadlock-prone ordering. Accessing rows in inconsistent order across concurrent transactions. Always acquire locks in a deterministic order (e.g., by primary key ascending).
-
Connection pool exhaustion. Long transactions combined with small pool sizes can starve other operations. Monitor pool checkout times via telemetry.
-
Assuming distributed ACID. Transactions across multiple databases or services do not automatically provide ACID guarantees. Use saga patterns or event sourcing for cross-service consistency.
-
Missing error handling on transaction failure. Not matching on
{:error, operation, changeset, changes_so_far}fromEcto.Multitransactions, losing diagnostic information.
Use Cases
Security Rating Calculation
The Prismatic Perimeter module calculates security ratings (A-F grades, 300-900 scores) for domains. Each rating calculation involves multiple database writes that must be atomic -- a partial rating update would leave the system in an inconsistent state.
OSINT Data Ingestion
When ingesting data from OSINT sources (ARES, Justice Registry, Shodan), the platform uses transactions to ensure that entity records, relationship edges, and audit logs are either all committed or all rolled back.
Agent State Management
The 530+ AIAD agents require consistent state transitions. When an agent moves from idle to executing, the state change, task assignment, and resource allocation all happen within a single transaction.
Compliance Auditing
NIS2 and ZKB compliance assessments involve creating assessment records, linking evidence artifacts, and updating compliance scores. ACID transactions ensure that audit trails are complete and tamper-evident.
Quality Gate Enforcement
The Quality DNA system records quality scores across 13 domains. Score updates, violation records, and trend data are written atomically to prevent quality metric inconsistencies.
Related Technologies
| Technology | Relationship to ACID Transactions |
|---|---|
| PostgreSQL | The primary ACID-compliant database engine used throughout the platform |
| Ecto | Elixir's database wrapper providing changeset-based ACID transaction support |
| CAP Theorem | Fundamental theorem describing tradeoffs affecting ACID in distributed settings |
| Event Sourcing | Alternative persistence pattern using immutable event streams |
| Saga Pattern | Distributed transaction pattern using compensating actions |
| Concurrency | Concurrent access patterns that ACID transactions safely handle |
| Distributed System | Architectures where ACID becomes more challenging to maintain |
| Schema | Database schema definitions enforcing the Consistency property |
| Validation | Input validation as the first line of defense for consistency |
| Connection Pooling | Resource management critical for transaction throughput |
See Also
- Database -- General database concepts and patterns
- Eventual Consistency -- The BASE alternative to ACID strong consistency
- Outbox Pattern -- Reliable event publishing combined with ACID transactions
- ETS -- In-memory storage that does not provide full ACID guarantees
- Idempotency -- Operation design that complements transaction retry strategies
Connect & Contribute
Created by Tomas Korcak (korczis) | Open Source under GHL