Sitemap
CRScube Blog

CRScube Blog

CRScube

Improving Safety R3 XML Import Performance with AI

12 min readOct 17, 2025

--

Introduction

Background

The core module of the cubeSAFETY R3 system, R3ImportServiceImpl, is responsible for parsing XML files in the ICH E2B(R3) format—an international regulatory standard for pharmaceutical safety reporting—and converting them into the system’s internal data structure. This service processes hundreds of Individual Case Safety Reports (ICSRs)daily, collected from global pharmaceutical companies. Each XML file can range from several megabytes to tens of megabytes in size, meaning the system must handle large volumes of data efficiently. As such, XML parsing performance and memory efficiency are critical to maintaining system stability.

In the initial implementation, the system used a DOM (Document Object Model)–based parsing approach, which loads the entire XML file into memory at once. This worked adequately when data volumes were small. However, as the XML files grew larger, the system began suffering from high memory consumption, frequent garbage collection (GC) pressure, and slower processing speeds, leading to persistent performance bottlenecks.

While incremental code-level optimizations had provided modest improvements, it became clear that a more fundamental solution was needed. To address this challenge, we decided to take a new approach—leveraging AI to optimize XML import performance in a more systematic and intelligent way.

Structural Analysis with AI

At first, I’ll admit — I was skeptical.
With a “Well, let’s just see what happens” mindset, I gave the AI a single prompt:

“Improve XML import performance.”

The result… was impressively wrong.
The AI created unrelated classes, generated unnecessary files, and even produced slower code — before confidently declaring, “Performance optimization completed successfully.”
At that moment, I couldn’t help but laugh in disbelief.

But that wasn’t the end of the story.
As I examined the misguided output, I realized that — almost accidentally — the AI had exposed the exact bottlenecks in our system.
Loading the entire XML into memory, repeatedly evaluating XPath expressions, creating redundant objects…
All the core issues were right there, hiding in plain sight.

So I changed my strategy.
Instead of asking the AI to “fix everything,” I broke down the problem into smaller, precise prompts. For example:

“How can we reduce CPU load when repeatedly evaluating XPath expressions?”
“Generate safe conversion code from DOM-based parsing to StAX.”

By narrowing the focus, the AI began producing practical, high-quality code snippets.

In the end, the process evolved into a collaborative workflow — the AI analyzed patterns and suggested directions, while we refined and validated the details.
It felt like working with a brilliant but inexperienced junior developer — one who might go off track if left alone, but delivers remarkable results when guided with clear, specific instructions.

Thanks to this iterative partnership, we were able to pinpoint the root causes much faster and establish a clear direction for deeper structural optimization.

Existing Architecture and Issues

Role of R3ImportServiceImpl

The R3ImportServiceImpl class serves as a core module of the cubeSAFETY R3 system. Its primary responsibility is to parse XML files in the E2B(R3) format and convert them into individual case entities, which are then stored in the database.

DOM-Based Parsing Architecture

The initial implementation relied on a DOM (Document Object Model)–based approach, which loads the entire XML document into memory before processing.
The overall workflow was as follows:

  1. Parse the entire XML file using DocumentBuilderFactory to create a DOM tree.
  2. Use XPath to extract Case nodes.
  3. Traverse and transform each case node sequentially.
  4. Store the transformed data in the database.

This approach worked reliably for relatively small XML files.
However, as the system began to handle larger XMLs — up to 20MB per file — both memory usage and GC (Garbage Collection) overhead increased significantly.

Although a 20MB XML file may not seem exceptionally large by modern standards, the DOM model inherently scales poorly with file size.
Because it must load the entire document tree into memory, performance degrades sharply as the file grows — making it unsuitable for high-volume safety data processing at scale.

Limitations of the DOM-Based Approach

While the DOM (Document Object Model) method is simple to implement, it is structurally inefficient for handling large XML files.

  • Memory Explosion: The entire XML is loaded into an in-memory object tree, often consuming hundreds of megabytes.
  • Increased GC Overhead: Frequent object creation and disposal lead to excessive CPU cycles spent on garbage collection.
  • Performance Degradation: With repeated GC pauses and traversal overhead, even a 9 MB XML could take up to 60 minutes to process.
  • Poor Scalability: Processing time increases disproportionately with file size.
  • Instability: High risk of OutOfMemoryError and process termination during large file imports.

In addition, the repeated compilation of XPath expressions for every query caused significant overhead.
Every time a case node was retrieved, the system recompiled the XPath pattern — leading to thousands of redundant evaluations, with CPU and GC usage spiking together.

Key Bottlenecks

1. DOM-Based Memory Model

The DOM parser loads the entire XML tree into memory. This means that reading a 10 MB XML file could internally generate hundreds of megabytes of object data.

  • Heap memory exhaustion when handling large XMLs
  • Frequent Full GCs, consuming excessive CPU resources
  • Severe performance drops caused by memory swapping and GC pauses

2. Repeated XPath Compilation

Every data extraction relied on XPath, and each time a new XPathExpression was compiled and validated:

XPathExpression expr = xpath.compile("//PORR_IN049016UV");
NodeList list = (NodeList) expr.evaluate(document, XPathConstants.NODESET);

When this operation was repeated thousands of times per XML, CPU load spiked dramatically, and GC overhead compounded.
This combination of DOM memory inefficiency and XPath redundancy was the root cause of the performance bottleneck in R3ImportServiceImpl.

Summary of Structural Limitations

Press enter or click to view image in full size

These structural bottlenecks could not be resolved through simple code-level optimizations.
To achieve fundamental improvement, we transitioned to a hybrid architecture that combines streaming and cachingtechniques.

However, we intentionally excluded a batch processing approach from this improvement scope, as it poses potential risks of data loss during the save process — an unacceptable trade-off for a safety-critical system like cubeSAFETY.

Performance Improvements

Improvement Strategy

The performance bottleneck in R3ImportServiceImpl did not stem from slow code alone—it originated from a processing architecture fundamentally unsuited for large-scale data handling.

Therefore, this effort was not about temporary code optimizations or query tuning. Instead, it focused on a complete architectural redesign of the XML import process.

The new approach can be summarized by two key principles:

Press enter or click to view image in full size

DOM vs. StAX

DOM (Document Object Model)

The DOM approach parses an XML document by loading the entire structure into memory as a tree of objects, allowing node-level traversal and manipulation.
While it offers intuitive access to any node and is simple to implement, it becomes increasingly inefficient for large XML files due to several inherent limitations:

Excessive Memory Usage: Memory consumption grows proportionally with XML size.

Heavy GC Overhead: Large numbers of temporary node objects trigger frequent garbage collection.

Performance Degradation: As file size increases, total load and traversal times grow exponentially.

As a result, the DOM model is unsuitable for large-scale XML processing, potentially leading to instability and system slowdowns.

StAX (Streaming API for XML)

StAX is a streaming, event-driven parsing approach that reads XML documents sequentially rather than loading them into memory all at once.
Unlike DOM, it processes only the relevant elements in real time and discards them immediately after use.
In other words, it parses the XML file line by line, triggering events as elements are encountered.

Key characteristics include:

Pull-Based Model: The developer explicitly requests the next event (nextEvent(), nextTag(), etc.) and processes only the required data.

Memory Efficiency: Only the currently processed node is kept in memory, enabling stable memory usage even for very large XML files.

Parallel and Streaming Friendly: Maintains constant memory footprint regardless of file size.

High Performance: Minimal object creation and reduced I/O overhead yield faster parsing speeds.

In short, StAX transforms XML parsing from a static, memory-bound operation into a lightweight, continuous data stream, providing the foundation for scalable and high-performance XML imports in cubeSAFETY R3.

1.Maintain Compatibility

  • Design a hybrid structure that runs StAX alongside DOM, rather than fully replacing the existing DOM-based code.
  • Automatic fallback to the DOM parser if an error occurs during StAX processing.
  • Minimize changes to existing business logic.

2.Performance-First Structural Optimization

  • Parse in segments using StAX’s event-driven model instead of loading the entire XML into memory.
  • Precompile and cache XPath evaluations to simplify lookups and eliminate repetitive computation.
Press enter or click to view image in full size

Performance Improvements

1) Use StAX for Initial Case-Node Extraction (Partial Sections)

Get CRScube Blog’s stories in your inbox

Join Medium for free to get updates from this writer.

The original R3ImportServiceImpl used DocumentBuilderFactory with a DOM-based parser. Because DOM loads the entire XML into memory, large files triggered sharp increases in memory usage and GC overhead—an inherent structural limitation.

To address this, we introduced a StAX (Streaming API for XML)–based partial streaming design. The whole XML is read sequentially, and only case-level nodes are split out via streaming; the remaining sections continue to use the existing DOM logic — i.e., a hybrid approach.

  • A useStaxProcessor flag toggles DOM ↔ StAX.
  • A new importFileStax() method supports sequential, per-case parsing.
  • For each case, only the required nodes are converted to DOM, keeping full compatibility with existing business logic.

This significantly reduces overall memory consumption and improves stability when ingesting large XML files.

2) Preprocessing to Simplify Nodes for DOM + XPath (Remove Comments and Inter-Node Whitespace)

In a DOM pipeline, the more superfluous nodes exist, the more objects the DOM tree creates, which hurts XPath traversal efficiency.

We added a file preprocessing step before DOM parsing to reduce noise:

  • Remove XML comments (<!-- -->)
  • Strip inter-node whitespace and newline characters

This shallows the DOM tree and reduces node count, eliminating unnecessary text-node hits during XPath traversal and improving parse speed — especially in complex regions like the DG–AE MATRIX.

3) Precompile and Cache XPath Expressions

Previously, XPath objects were created and evaluated each time, creating a CPU bottleneck under heavy loads. We now precompile XPath expressions and cache them:

private static final Map<String, XPathExpression> 
COMPILED_XPATHS = new ConcurrentHashMap<>();
private static final ThreadLocal<XPath> XPATH_THREAD_LOCAL =
ThreadLocal.withInitial(() ->
XPathFactory.newInstance().newXPath());
  • Frequently used XPath expressions are compiled ahead of time and stored in a ConcurrentHashMap.
  • ThreadLocal provides a thread-safe XPath instance per thread.
  • A cache size limit (max 500) prevents memory overuse.

Note: In areas like the DG–AE MATRIX, where XPath strings include UUIDs and are generated dynamically, the benefit of caching is naturally limited.

4) Optimize String Handling and XML Escaping

For replacing UUIDs and special tokens inside XPath or XML fragments, we replaced regex-based replaceAll() with String.replace() to avoid unnecessary regex compilation overhead:

private String optimizeUuidReplacement(String xpath, String uuidReaction, String uuidDrug) {
String result = xpath;
if (uuidReaction != null && result.contains(UUID_REACTION)) {
result = result.replace(UUID_REACTION, uuidReaction);
}
if (uuidDrug != null && result.contains(UUID_DRUG)) {
result = result.replace(UUID_DRUG, uuidDrug);
}
return result;
}

We also added a straightforward, string-based XML escaping routine for special characters (&, <, >, ", '):

private String escapeXml(String text) {
if (text == null) return "";
return text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&apos;");
}

Performance Improvement Results

Press enter or click to view image in full size

By combining DOM → StAX conversion and XPath caching, we achieved more than 2× overall performance improvement in XML import processing.

Conclusion

This R3 Import performance enhancement was not merely about speeding things up — it was about fundamentally rethinking the architecture.
XML parsing inefficiencies cannot be solved with a few lines of code optimization. The transition from DOM to StAXrepresented more than a change in technology — it was a shift in paradigm, redesigning how data is processed to be streaming-oriented rather than memory-bound.

As a result, the system became lighter, faster, and far more stable, with visible reductions in import bottlenecks.

Of course, some limitations remain.
To maintain stability, this phase focused on minimal changes to existing business logic. Therefore, as long as DOM parsing remains partially in use, optimization has its ceiling.

That’s why our next step is to fully replace DOM with a StAX-based architecture.
We’ve already begun simulations comparing both methods to validate their real-world performance differences.

Stay tuned — the next evolution of cubeSAFETY R3 will be faster than ever. 🚀

Press enter or click to view image in full size

Appendix. DOM vs. StAX Performance Comparison Simulation

The performance simulation was conducted using three test scenarios to verify the real-world efficiency of the StAX parser compared to DOM.

  1. Full Data Extraction Test — Parse the entire XML and compare data accuracy and processing time.
  2. Memory-Constrained Simulation — Measure memory usage under limited heap conditions.
  3. Bulk Processing Test — Repeatedly process the same XML file ten times to simulate large-scale batch imports.

1. Full Data Extraction Test

Purpose: Compare DOM and StAX parsing performance by extracting all data from an E2B XML file.

private static void testFullDataExtraction(File xmlFile) throws Exception {
// DOM Test
long[] domTimes = new long[TEST_ITERATIONS];
E2bReport[] domResults = new E2bReport[TEST_ITERATIONS];

// Warm-up Phase
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
for (int i = 0; i < WARMUP_ITERATIONS; i++) {
Document doc = builder.parse(xmlFile);
extractCompleteDataFromDom(doc);
}

// Actual Measurement
for (int i = 0; i < TEST_ITERATIONS; i++) {
long startTime = System.nanoTime();
Document document = builder.parse(xmlFile);
domResults[i] = extractCompleteDataFromDom(document);
long endTime = System.nanoTime();
domTimes[i] = endTime - startTime;
}

// StAX Test
long[] staxTimes = new long[TEST_ITERATIONS];
E2bReport[] staxResults = new E2bReport[TEST_ITERATIONS];

XMLInputFactory xmlFactory = XMLInputFactory.newInstance();
// Warm-up Phase
for (int i = 0; i < WARMUP_ITERATIONS; i++) {
XMLStreamReader reader = xmlFactory.createXMLStreamReader(new FileInputStream(xmlFile));
extractCompleteDataFromStax(reader);
reader.close();
}

// Actual Measurement
for (int i = 0; i < TEST_ITERATIONS; i++) {
long startTime = System.nanoTime();
XMLStreamReader reader = xmlFactory.createXMLStreamReader(new FileInputStream(xmlFile));
staxResults[i] = extractCompleteDataFromStax(reader);
reader.close();
long endTime = System.nanoTime();
staxTimes[i] = endTime - startTime;
}

// Result Output
printTestResults("DOM (Full Data Extraction)", domTimes, domResults[0]);
printTestResults("StAX (Full Data Extraction)", staxTimes, staxResults[0]);

// Result Output
validateResults(domResults[0], staxResults[0]);
}

Result

1. Full Data Extraction Test

DOM (Full Extraction):

  • Average Time: 14,448 ms
  • Extracted Data: 1 patient, 174 drugs, 1 adverse event

StAX (Full Extraction):

  • Average Time: 127 ms
  • Extracted Data: 1 patient, 174 drugs, 1 adverse event

Result Validation: ✅ Passed

2. Memory-Constrained Simulation Test

Purpose: Compare the memory usage of DOM and StAX.

private static void testMemoryConstrainedParsing(File xmlFile) throws Exception {
Runtime runtime = Runtime.getRuntime();

System.out.println("Memory Usage Comparison:");

// DOM Memory Usage
System.gc();
long beforeDom = runtime.totalMemory() - runtime.freeMemory();

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(xmlFile);
E2bReport domReport = extractCompleteDataFromDom(document);

long afterDom = runtime.totalMemory() - runtime.freeMemory();
long domMemory = afterDom - beforeDom;

System.out.println("DOM Memory Usage: " + domMemory / 1024 + " KB");

// Memory Release
document = null;
domReport = null;
System.gc();
Thread.sleep(100);

// StAX Memory Usage
long beforeStax = runtime.totalMemory() - runtime.freeMemory();

XMLInputFactory xmlFactory = XMLInputFactory.newInstance();
XMLStreamReader reader = xmlFactory.createXMLStreamReader(new FileInputStream(xmlFile));
E2bReport staxReport = extractCompleteDataFromStax(reader);
reader.close();

long afterStax = runtime.totalMemory() - runtime.freeMemory();
long staxMemory = afterStax - beforeStax;

System.out.println("StAX Memoery Usage: " + staxMemory / 1024 + " KB");
System.out.println("Memeory Savings: " + (domMemory - staxMemory) / 1024 + " KB");
}

Result

2. Memory-Constrained Simulation Test

Memory Usage Comparison:

  • DOM Memory Usage: 77,625 KB
  • StAX Memory Usage: 1,681 KB
  • Memory Saved: 75,944 KB

3. Large-Scale (Bulk) Simulation Test

Purpose: Compare large-volume processing performance by repeatedly parsing the same XML file multiple times.

private static void testBulkProcessing(File xmlFile) throws Exception {
final int BULK_COUNT = 10;

// DOM Bulk Processing
long startTime = System.nanoTime();
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();

List<E2bReport> domReports = new ArrayList<>();
for (int i = 0; i < BULK_COUNT; i++) {
Document document = builder.parse(xmlFile);
E2bReport report = extractCompleteDataFromDom(document);
domReports.add(report);
}
long domTotalTime = System.nanoTime() - startTime;

// StAX Bulk Processing
startTime = System.nanoTime();
XMLInputFactory xmlFactory = XMLInputFactory.newInstance();

List<E2bReport> staxReports = new ArrayList<>();
for (int i = 0; i < BULK_COUNT; i++) {
XMLStreamReader reader = xmlFactory.createXMLStreamReader(new FileInputStream(xmlFile));
E2bReport report = extractCompleteDataFromStax(reader);
reader.close();
staxReports.add(report);
}
long staxTotalTime = System.nanoTime() - startTime;

System.out.println("DOM Bulk Processing (" + BULK_COUNT + "runs): " + domTotalTime / 1_000_000 + " ms");
System.out.println("StAX Bulk Processing (" + BULK_COUNT + "runs): " + staxTotalTime / 1_000_000 + " ms");
System.out.println("Processed Reports: " + domReports.size() + " vs " + staxReports.size());

double speedup = (double) domTotalTime / staxTotalTime;
System.out.println("StAX Speedup: " + String.format("%.2f", speedup) + "faster");
}

Result

3. Large-Scale Simulation Test (10 Consecutive Runs)

  • DOM Bulk Processing (10 runs): 173,315 ms
  • StAX Bulk Processing (10 runs): 1,929 ms
  • Processed Reports: 10 vs. 10
  • StAX Speedup: 89.85×

As a result, StAX proved to be approximately 89 times faster than DOM, while reducing memory usage by more than 98%.
This remarkable improvement was achieved not through minor code tweaks, but through a fundamental architectural shift, which brought visible gains in both stability and processing speed.

But we didn’t stop there.
Building on these results, we began a full-scale architectural redesign of the R3 Import pipeline — restructuring it around a streaming-first model and removing bottlenecks across the data flow to push both performance and reliability to the next level.

These enhancements will soon be incorporated into the official release of cubeSAFETY R3, delivering an XML Import engine that is lighter, faster, and more robust than ever, setting a new benchmark for data processing efficiency.

--