Demo bombs are falling- avoid them
- Mark Kendall
- Aug 8
- 5 min read
You’re right—this is fixable. The pattern that stops “demo-day null bombs” is:
1. Validate before you map; map before you send.
2. Only send allowed and non-null fields—unless you explicitly intend to null them.
3. Fail fast with clear 4xx errors; retry only real 5xx/429s.
4. Defend demos with a “guardrail mode.”
Here’s a tight, battle-tested way to do it in Spring Boot.
1) Strong input contracts (Bean Validation)
Make your inbound DTOs strict. If a field is required for your Salesforce object (create) or your routing logic, enforce it here, not at Salesforce.
import jakarta.validation.constraints.*;
public record NautobotInboundDto(
@NotBlank String circuitId,
@NotBlank String accountNumber,
@NotBlank String action, // "create" | "update" | "upsert"
@Size(max = 255) String description,
// Use wrapper types to allow nulls intentionally:
Integer bandwidthMbps,
@Pattern(regexp="ACTIVE|SUSPENDED|DECOMMISSIONED") String status
) {}
// Controller
@RestController
@RequestMapping("/api/integrations")
@Validated
public class IntegrationController {
private final IntegrationService svc;
public IntegrationController(IntegrationService svc){ this.svc = svc; }
@PostMapping("/nautobot-to-salesforce")
public ResponseEntity<?> route(@Valid @RequestBody NautobotInboundDto dto){
var result = svc.process(dto);
return ResponseEntity.ok(result);
}
}
Add a global handler so “manager hacks a field” → clean 400 with human text, not a 500:
@RestControllerAdvice
class GlobalErrors {
@ExceptionHandler(MethodArgumentNotValidException.class)
ResponseEntity<?> handleValidation(MethodArgumentNotValidException ex){
var errs = ex.getBindingResult().getFieldErrors().stream()
.map(fe -> Map.of("field", fe.getField(), "message", fe.getDefaultMessage()))
.toList();
return ResponseEntity.badRequest().body(Map.of(
"code","VALIDATION_ERROR",
"message","Fix the highlighted fields and try again.",
"details", errs
));
}
}
2) Null-safe mapping (send only what you mean)
Use MapStruct to ignore nulls by default (so PATCH doesn’t accidentally erase fields). Add an explicit nulls mechanism if you do want to clear fields.
@Mapper(componentModel = "spring",
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface SalesforceMapper {
// Map your inbound DTO to the minimal SF payload
@Mapping(target = "Name", source = "description") // example
SalesforcePayload toSalesforce(NautobotInboundDto in);
}
If you ever intend to set a field to null in Salesforce, carry an “explicitNulls” set:
public record SalesforcePayload(Map<String,Object> fields, Set<String> explicitNulls) {}
Before send:
Map<String,Object> body = new HashMap<>(payload.fields());
payload.explicitNulls().forEach(f -> body.put(f, null)); // only fields you mean to null
3) Preflight against Salesforce metadata (stop bad requests before they go out)
Cache the object’s Describe metadata (nillable, updateable, required). Then validate the payload locally.
@Service
class SalesforceDescribeCache {
private final SalesforceClient sf;
private final ConcurrentMap<String, SObjectDescribe> cache = new ConcurrentHashMap<>();
SObjectDescribe describe(String object){
return cache.computeIfAbsent(object, sf::fetchDescribe); // refresh on TTL in background
}
}
@Service
class PreflightValidator {
private final SalesforceDescribeCache meta;
PreflightValidator(SalesforceDescribeCache meta){ this.meta = meta; }
void validate(String object, Map<String,Object> body, Set<String> explicitNulls, String op){
var d = meta.describe(object);
// 1) Required on create
if ("create".equals(op)) {
d.requiredFields().forEach(req -> {
if (!body.containsKey(req)) throw bad("MISSING_REQUIRED", "Missing required field: "+req);
});
}
// 2) Reject updates to non-updateable fields
for (var f : body.keySet()){
if (!d.isUpdateable(f)) throw bad("FIELD_NOT_UPDATEABLE", "Field not updateable: "+f);
}
// 3) If nulling, ensure nillable
for (var f : explicitNulls){
if (!d.isNillable(f)) throw bad("FIELD_NOT_NILLABLE", "Cannot null non-nillable field: "+f);
}
}
private RuntimeException bad(String code, String msg){
return new IllegalArgumentException(code + ":" + msg);
}
}
Hook this into your service:
@Service
class IntegrationService {
private final SalesforceMapper mapper;
private final PreflightValidator pre;
private final SalesforceClient sf;
// ...
public Map<String,Object> process(NautobotInboundDto in){
var sfPayload = mapper.toSalesforce(in);
var op = chooseOperation(in); // create/update/upsert
pre.validate("Case", sfPayload.fields(), sfPayload.explicitNulls(), op);
// finally send with resilient client
return sf.send(op, "Case", sfPayload);
}
}
4) Smart retry policy (never retry bad data)
• Do not retry 400/401/403/404 from Salesforce—surface a 4xx with the exact offending field(s).
• Do retry 429 or 5xx with exponential backoff + jitter and cap attempts.
• DLQ only when you’ve exhausted retries on transient errors.
@Component
class SfHttp {
ResponseEntity<String> call(HttpEntity<?> req, URI uri, HttpMethod method){
try {
return rest.exchange(uri, method, req, String.class);
} catch (HttpStatusCodeException ex){
int s = ex.getStatusCode().value();
if (s >= 500 || s == 429) throw new TransientSfException(ex); // will be retried
throw new PermanentSfException(parseSalesforceError(ex)); // no retry
}
}
}
5) A single, boring, crystal-clear error shape
Managers love to see why it failed. Standardize:
{
"code": "FIELD_NOT_NILLABLE",
"message": "Cannot null non-nillable field: OwnerId",
"referenceId": "4f7e8b50-1e0e-4a3c-b8af-c7f2d2b1a0b2",
"details": [
{"field": "OwnerId", "reason": "NON_NILLABLE"}
]
}
Implement with @RestControllerAdvice plus a correlation ID (MDC) you add at the edge.
6) Demo Guardrails (stop “hack-n-break”)
Flip on a DEMO_SAFE mode:
• Readonly allow-list of fields that can be changed during demos.
• Reject unknown/extra fields in the inbound JSON (@JsonIgnoreProperties(ignoreUnknown = false)).
• Schema-driven forms on the front end (OpenAPI or JSON Schema) so the UI prevents null/invalid combos.
• Golden test payloads seeded and locked; “manager edits” happen through a guarded UI that enforces constraints.
Server-side filter:
@Component
class DemoGuardFilter extends OncePerRequestFilter {
@Value("${demo.safe:false}") boolean demoSafe;
private static final Set<String> ALLOW = Set.of("description","status","bandwidthMbps");
@Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
if (demoSafe && req.getRequestURI().contains("/nautobot-to-salesforce") && "POST".equals(req.getMethod())) {
var body = req.getInputStream().readAllBytes();
var node = new ObjectMapper().readTree(body);
for (Iterator<String> it = node.fieldNames(); it.hasNext();) {
var f = it.next();
if (!ALLOW.contains(f)) {
res.setStatus(400);
res.setContentType("application/json");
res.getWriter().write("""
{"code":"DEMO_GUARD","message":"Field not allowed in demo: %s"}""".formatted(f));
return;
}
}
// re-wrap body for downstream if needed (use ContentCachingRequestWrapper in real code)
}
chain.doFilter(req, res);
}
}
7) Observability that actually helps
• Correlation ID per request; propagate to Salesforce call.
• Structured logs with the mapped payload (mask secrets), SF status code, and SF error errorCode/fields.
• Metrics: count of validation failures, SF 4xx by field, SF 5xx, retry attempts, DLQ depth.
8) Tests that catch the “nulls”
• Property-based tests generating random nulls/missing fields to ensure validator blocks bad cases.
• Contract tests with WireMock covering: create/update, non-nillable null, non-updateable field, 429 backoff, 503 retry then success.
• Schema tests: if you keep JSON Schema for inbound, validate with it in unit tests.
9) A tiny “good payload” example
What you actually send to Salesforce for an update/PATCH should be only what changed—and never accidental nulls:
{
"Description": "Upgrade to 10Gbps",
"Status__c": "ACTIVE",
"Bandwidth__c": 10000
}
If you intentionally clear a field (and it’s nillable), include it as null on purpose:
{
"Customer_Notes__c": null
}
TL;DR “No-more-embarrassment” checklist
• ✅ Bean Validation on inbound DTOs (server and UI).
• ✅ MapStruct with IGNORE null strategy; explicit set for intended nulls.
• ✅ Preflight with cached Salesforce Describe (required/nillable/updateable).
• ✅ Unified error response + correlation ID.
• ✅ Retry only 429/5xx; DLQ after capped backoff.
• ✅ Demo Safe mode: allow-list fields, reject unknowns.
• ✅ Logs/metrics that pinpoint which field blew up.
• ✅ Contract tests for null/updateability/nillability paths.
If you want, I’ll wire this into your existing project: drop-in MapStruct config, a DescribeClient, the validator, and the @RestControllerAdvice.

Comments