Never Use Integer for External Partner IDs
A partner API spec declared a field as 'Integer'. We implemented it as Java Integer. Months later the partner returned values beyond 2,147,483,647. A production hotfix across three repositories followed. The fix took one evening; the lesson is permanent.
What Happened
A payment partner's API spec defined two fields as Integer:
quotation.id : Integer
transaction.id : Integer
Our adapter library implemented these as Java Integer (signed 32-bit, max: 2,147,483,647).
In production, the partner began returning ID values exceeding the signed 32-bit integer range. Java silently overflowed or threw a deserialization exception. Transaction failures followed for affected payment corridors. A same-evening hotfix was required across three repositories.
Why This Keeps Happening
The spec was ambiguous
Modern API specs should be explicit about numeric ranges:
| Standard | How to be explicit |
|---|---|
| OpenAPI 3 | format: int32 or format: int64 |
| JSON Schema | minimum / maximum constraints |
| Protobuf | int32, int64, uint64 |
Integer without a format qualifier is ambiguous. Our team followed the spec literally and chose int32. The partner was producing values beyond 2^31 - 1. Neither side was wrong — the spec simply didn't define the range.
Tests used small values
// Insufficient — does not test boundaries
transaction.setId(123);
// What was needed
transaction.setId(BigInteger.valueOf(2_147_483_648L)); // int32 max + 1
Tests using 123 or 1122 cannot catch integer overflow. There were no test cases at or beyond Integer.MAX_VALUE.
The type was defined in a shared library
Because the type lived in a shared adapter library, one wrong assumption required a coordinated hotfix across all consumers:
partner-adapter-lib ← type definition
↓
fx-service ← uses QuoteResponse.id
payments-service ← uses TransactionResponse.id in mapper + submit flow
The Hotfix
Change 1 — adapter library: Integer → BigInteger
// Before
Integer id;
// After
BigInteger id;
Change 2 — consumer services: null-safety audit
The type change exposed a secondary bug. Integer auto-unboxed to int would throw NullPointerException early if null — easy to catch. BigInteger is always an object, so .toString() on a null BigInteger also throws NullPointerException but looks safe at a glance.
Every call site that converted the ID to a string had to be audited:
// Unsafe — throws NPE if BigInteger id is null
return num.toString();
.map(Object::toString)
// Safe
return num == null ? null : num.toString();
.map(String::valueOf)
The Lessons
Lesson 1 — Use String for all external partner IDs
Identifiers returned by external APIs should be stored and passed as String, not as numeric types — even when the current values look numeric.
// Fragile — creates a dependency on the partner's current ID range
Integer quotationId;
Long quotationId;
BigInteger quotationId;
// Robust — no overflow, no precision loss, survives format changes
String quotationId;
Why String is always right:
- Today the value is numeric. Tomorrow it may become a UUID, an alphanumeric reference, or a 128-bit snowflake.
- Numeric types impose range constraints the partner never agreed to.
Stringhas no overflow, no precision loss, no deserialization edge cases.
Rule: All external partner transaction IDs, quotation IDs, and payment references should be
Stringin both adapter DTOs and internal models.
Lesson 2 — Validate spec ambiguity during partner onboarding
When a vendor spec declares a field as Integer without an explicit format, do not assume int32. Raise the ambiguity formally before implementation.
Partner onboarding checklist for numeric fields:
- Is the numeric range explicitly documented (
int32/int64)? - Have we seen real production sample payloads with large values?
- Can this ID ever exceed
2,147,483,647(int32 max)? - Should this ID be treated as an opaque string?
If the spec is ambiguous: request written clarification from the vendor. This shifts accountability and protects the team if an incident occurs later.
Lesson 3 — Add boundary test cases for all external numeric fields
// For every numeric ID field from an external partner:
@Test
void shouldHandleInt32Overflow() {
// int32 max + 1
transaction.setId(BigInteger.valueOf(2_147_483_648L));
assertThat(mapper.toInternal(transaction).getPartnerId())
.isEqualTo("2147483648");
}
@Test
void shouldHandleLargeIds() {
transaction.setId(new BigInteger("99999999999999999999"));
assertThat(mapper.toInternal(transaction).getPartnerId())
.isEqualTo("99999999999999999999");
}
Rule: Every external numeric ID field must have at least one test case with a value exceeding
Integer.MAX_VALUE.
Lesson 4 — Type changes in shared libraries require null-safety audit in consumers
When a field type changes from Integer to BigInteger in a shared library:
Integerauto-unboxed tointthrowsNullPointerExceptionearly and visibly — easy to catch in testsBigIntegeris always an object —.toString()on null also throwsNullPointerException, but looks safe and may be missed
Any shared library upgrade that changes field types must include a null-safety audit of all downstream call sites on the changed field.
Lesson 5 — Isolate partner DTOs from internal models
The cascading change across three repositories happened because the partner model type was tightly coupled to the internal business flow. A proper adapter layer contains the blast radius:
Partner API Response (Integer id)
↓
Adapter Layer ← only code that knows partner types; normalizes here
↓ map to String
Internal Canonical Model (String quotationId)
↓
Business Services (fx-service, payments-service)
If the internal canonical model had used String quotationId from the start, only the adapter layer would have needed a change. The two downstream services would have been untouched.
Summary
| Standard | Applies to |
|---|---|
External partner IDs → String |
All partner integrations |
| Boundary test cases for numeric fields | All adapter library tests |
| Null-safety audit on type changes | All shared library upgrades |
| Written clarification for ambiguous specs | Partner onboarding process |
| Canonical internal model separate from partner DTO | All partner adapters |
Based on a production hotfix in a Spring Boot microservices payment platform. Partner and company identifiers removed.