Apex DML error

DUPLICATE_VALUE: duplicate value found

"DUPLICATE_VALUE: duplicate value found" — what it means and how to fix it

Two different mechanisms produce this one status code: a unique field constraint, which is exact and enforced by the database, and Duplicate Management, which is fuzzy and enforced by a matching rule. The message tells you which. In test suites it's almost always the first, and almost always a bulk insert colliding with itself.

Quick answer

If the message names a field and a record Id, it's a unique constraint — stop using constant values in test data and derive them from a counter or UserInfo.getOrganizationId(). If it names a duplicate rule instead, it's Duplicate Management — either make the record less similar, or set DMLOptions.DuplicateRuleHeader.allowSave = true where saving a known duplicate is intentional.

What the error means, mechanically

A unique field is backed by a real uniqueness constraint. Before the row is committed, the platform checks the value against every other record of that object — including the other rows in the same DML statement. Any collision rejects the row:

text
Insert failed. First exception on row 0; first error:
  DUPLICATE_VALUE, duplicate value found: External_Id__c duplicates value
  on record with id: a015g00000ABCDeAAP

That trailing record Id is the most useful thing in the message: it points at the record you collided with. When the Id is missing or empty, the collision was against another row inside your own DML statement, which never got an Id.

Duplicate Management is a different system entirely — matching rules with fuzzy comparison, wrapped in duplicate rules that either alert or block. A blocking rule rejects the save with the same DUPLICATE_VALUE status code but names the rule rather than a field. Two mechanisms, one code, which is why searching for the unique field when a duplicate rule fired wastes an afternoon.

Three properties of unique fields that surprise people:

  • Case-insensitive by default. A unique text field treats ACME and acme as the same value unless it's configured case-sensitive.
  • Nulls don't collide. Any number of records may leave a unique field blank; only populated values are compared.
  • External Id is orthogonal. Marking a field External Id does not make it unique — that's a separate checkbox. An External Id that isn't unique will happily accept duplicates until an upsert on it fails with a different error.

The causes, ranked by how often they actually happen

1. A bulk insert colliding with itself

The most common cause in test code by a wide margin. A loop builds 200 records to prove bulkification, and every one of them carries the same literal in a unique field. The first row saves, the second rejects, and the error blames a record Id that didn't exist a moment ago.

apex
// Fails on row 1: every record has the same External_Id__c
List<Product__c> products = new List<Product__c>();
for (Integer i = 0; i < 200; i++) {
  products.add(new Product__c(Name = 'Widget', External_Id__c = 'SKU-001'));
}
insert products;

// Works: derive the unique value from the loop index
for (Integer i = 0; i < 200; i++) {
  products.add(new Product__c(
    Name = 'Widget ' + i,
    External_Id__c = 'SKU-' + String.valueOf(i).leftPad(4, '0')
  ));
}
insert products;

2. Constant test data reused across methods and classes

Each test method runs in its own transaction and rolls back, so a constant is usually safe within a class. It stops being safe the moment a @testSetup creates the same value the test method creates, or a shared factory hands the same literal to two records in one transaction. Parallel execution doesn't cause this — rollback isolation still holds — but shared factories make it look like it does.

apex
// A factory that cannot collide with itself
public class TestDataFactory {
  private static Integer counter = 0;

  public static String uniqueKey(String prefix) {
    counter++;
    return prefix + '-' + String.valueOf(DateTime.now().getTime()) + '-' + counter;
  }

  public static Account newAccount() {
    return new Account(
      Name = 'Test Account ' + counter,
      External_Id__c = uniqueKey('ACC')
    );
  }
}

A static counter is enough inside one transaction. If you also need uniqueness against data already in the org — integration tests, or SeeAllData=true — mix in UserInfo.getOrganizationId() or a timestamp as above.

3. The value already exists in the org

Straightforward when the message names a record Id: query it and decide whether you meant to update rather than insert. upsert on the External Id field is usually the intended operation:

apex
// Insert-or-update keyed on the external system's identifier
List<Product__c> incoming = buildFromPayload(payload);
upsert incoming External_Id__c;

4. A Duplicate Rule set to Block

Fuzzy matching on names, emails, or addresses. Test data made of near-identical records ("Test Account 1", "Test Account 2", all with the same billing address) is exactly what a standard matching rule is built to catch. When saving the duplicate is intentional, say so explicitly rather than making the data weirder:

apex
Database.DMLOptions dml = new Database.DMLOptions();
dml.DuplicateRuleHeader.AllowSave = true;

Database.SaveResult sr = Database.insert(acct, dml);
if (!sr.isSuccess()) {
  for (Database.Error e : sr.getErrors()) {
    System.debug(e.getStatusCode() + ': ' + e.getMessage());
  }
}

5. An upsert whose external Id matches more than one record

Rarer, and it reads oddly: the upsert itself reports a duplicate because the key it was given is ambiguous. That means the External Id field is not marked unique — fix the field definition rather than the code.

Partial success: saving the rows that are fine

Statement DML (insert records;) is all-or-nothing: one duplicate fails the entire list. When importing data, Database.insert(records, false) saves what it can and hands you a result per row — which also gives you a far better error report than the first exception on row 0.

apex
List<Database.SaveResult> results = Database.insert(records, false);

for (Integer i = 0; i < results.size(); i++) {
  if (results[i].isSuccess()) {
    continue;
  }
  for (Database.Error e : results[i].getErrors()) {
    if (e.getStatusCode() == StatusCode.DUPLICATE_VALUE) {
      System.debug('Row ' + i + ' duplicate on ' + e.getFields() + ': ' + e.getMessage());
    }
  }
}

e.getFields() is the fastest way to identify the colliding field programmatically — no string parsing of the message.

Reproduce the unique-field collision locally

Nimbus runs your Salesforce Apex tests on your machine against an embedded PostgreSQL with your project's schema, and it enforces unique fields the way the platform does. Fields your metadata marks unique are checked both against existing rows and within the same DML batch, case-insensitively for text — so the 200-identical-records bulk test fails locally with DUPLICATE_VALUE, duplicate value found in a DmlException, seconds after you write it, with no deploy.

Being precise about the scope: this covers the unique-constraint mechanism, which is causes 1 through 3 and the one that dominates test failures. Duplicate Management rules are not evaluated locally — the Apex surface exists so code using Database.DMLOptions.DuplicateRuleHeader compiles and runs, but a local pass does not prove a blocking duplicate rule won't fire in the org.

bash
# Reproduce the collision without a deploy
nimbus test "ProductImportTest.testBulkInsert"

# See the failing DML and the operations that preceded it
nimbus explain ProductImportTest.testBulkInsert