In fixed-asset accounting, importing a spreadsheet means reconstructing a business model: who purchased an asset, what it cost, which accounting rules apply, and which physical units the business now owns. If those relationships are wrong, the consequences travel into custody records, depreciation schedules, and disposal reports.
I built the import pipeline in FAMS, a fixed asset management system using Flutter, Spring Boot, and PostgreSQL. The business problem was to bring existing asset registers into the application without making accountants manually recreate every vendor, asset definition, and purchase. The engineering problem was to preserve accounting relationships while handling validation, partial failures, and growing datasets.
Start with the asset lifecycle
The data model separates three responsibilities. A purchase header holds invoice-level information. Its items hold asset quantities and financial details. Individual labels identify the physical units that continue through allocation, verification, depreciation, and disposal.
Consider an invoice for ten identical laptops. The invoice is one financial transaction, but the laptops can go to different employees and eventually have different disposal histories. Separating financial ownership from physical identity lets those histories evolve without rewriting the purchase.
That model shapes ingestion. The import must create connected domain records, rather than copy spreadsheet cells into one flat table.
Put accounting decisions behind a service boundary
The architecture follows a clear path:
Excel → Flutter parsing and validation → REST bulk endpoint → Spring Boot services → JPA repositories → PostgreSQL
Flutter interprets the workbook and presents validation feedback. The backend owns reference resolution, dependency checks, and persistence. Controllers coordinate requests and responses; services implement the business operation; repositories handle relational access.
This keeps accounting behavior reusable across clients. Changing a screen should not change how a purchase connects to its asset definitions or financial rules. It also gives debugging a useful structure: distinguish a parsing problem from a reference-resolution problem or a database write failure.
Resolve dependencies in business order
Spreadsheet users work with names. Database relationships work with identifiers. Bridging that gap is a substantial part of the pipeline.
For an asset item, the service resolves its block, then its sub-block using the resolved parent. It resolves the depreciation rule using both its name and method, and then finds or creates the asset definition with those references. Vendor resolution similarly connects the purchase to the appropriate company-specific master.
The order matters. A sub-block needs its block identity; an asset definition needs the accounting references that describe it. Resolving those prerequisites inside the import reduces the separate setup work required before an accountant can load a register.
Company identity is included in relevant lookups. Two companies can use the same asset or vendor name without those names implying the same record.
Cache repeated resolutions within the import
A register often repeats the same vendor, asset definition, unit, and depreciation rule. Looking up each reference again for every item repeats work without adding business value.
The bulk request creates an import context with maps for these resolutions. Keys reflect the domain: company and vendor name; company, block, and sub-block name; or company, rule name, and depreciation method. When the service creates a vendor or asset definition, it remembers that result for subsequent entries.
This makes reuse explicit at the application layer, including across separate transaction calls. The cache lifetime matches the import operation, keeping the optimization close to the workflow it serves.
Make one purchase the unit of consistency
The bulk controller processes submitted transactions individually and returns an outcome for each. Each service call uses a database transaction to create the purchase and its related items together.
This boundary serves two business needs. A failed item should not leave its purchase half-created. At the same time, one bad purchase should not undo other valid purchases in the upload. The controller records each success or failure and continues, giving the interface enough information to direct corrections.
The response position identifies a submitted transaction; the frontend maps that outcome back to the workbook. This distinction matters when several spreadsheet entries belong to one business transaction.
Bound Hibernate's managed state
Within a large purchase, JPA can accumulate managed entities as items are created. I added a flush-and-clear step every 20 items, then reacquired the purchase before continuing:
// Simplified excerpt inside the transactional item loop.
entityManager.persist(item);
if ((index + 1) % 20 == 0) {
entityManager.flush();
entityManager.clear();
purchase = entityManager.find(Purchase.class, purchaseId);
}
Flushing synchronizes pending writes; clearing releases managed state and detaches the parent too, which explains the reacquisition. The purchase transaction remains the consistency boundary. This addresses persistence-context growth while preserving the business operation's transaction semantics.
Results
The pipeline was client-tested with 956 records, and I personally tested imports up to 10,000 records.
The architecture connects those workload results to a practical outcome: accountants can load existing registers through a familiar spreadsheet workflow, while the application constructs the relationships needed for the rest of the asset lifecycle. Ordered dependency resolution removes setup friction, transactional writes preserve connected purchases, cached lookups reduce repeated work, and per-entry feedback makes corrections manageable.
Top comments (0)