Designing Industrial Barcode Traceability with ASP.NET Core
During an internship, I worked on an industrial traceability application for a packaging workflow. The concept is straightforward: operators create boxes, scan uniquely identified cable packages into those boxes, and the system tracks everything. The reality is more nuanced, because in an industrial environment, reliability and data integrity matter more than feature count.
I cannot share confidential details about the specific industrial context, so I will describe the system in generic terms. The patterns and decisions, however, come from real development and real constraints.
The Core Concept
A box is a container that holds multiple packages. Each package has a unique identifier, typically encoded as a barcode. An operator scans packages as they are placed into a box. When a box reaches its target capacity, it is marked as complete and moves to the next stage of the workflow.
The system needs to guarantee several things:
- A package cannot be scanned into two different boxes
- A package cannot be scanned into the same box twice
- A box has a defined lifecycle (open, in progress, completed)
- Certain operations require elevated permissions
- Every significant action is recorded in an audit trail
Box Creation and Identification
When an operator creates a box, the system generates a unique identifier. This identifier is used throughout the box's lifecycle for lookups, references in reports, and audit log entries.
The box tracks its status (open, in progress, completed), the operator who created it, the timestamp, and the list of packages it contains. The system enforces a maximum capacity: once a box reaches its target number of packages, it is automatically marked as complete.
Barcode Scanning and Duplicate Prevention
The scanning workflow is the most critical path in the application. When an operator scans a package barcode, the system must:
- Validate that the barcode format is correct
- Check that the package has not already been assigned to any box
- Check that the package has not already been scanned into the current box
- Add the package to the box and record the scan timestamp
- Update the box's completion status if the target is reached
The duplicate prevention is the hardest part. It requires a global uniqueness check: not just within the current box, but across all boxes in the system. This is enforced at both the application level (a query before insert) and the database level (a unique constraint on the package identifier).
public async Task<IActionResult> ScanPackage(string boxId, [FromBody] ScanRequest request)
{
var package = await _context.Packages
.FirstOrDefaultAsync(p => p.Barcode == request.Barcode);
if (package != null && package.BoxId != null)
{
return Conflict(new { message = "Package already assigned to a box." });
}
// ... proceed with assignment
}
The database constraint is the final safety net. Even if two concurrent requests pass the application-level check, the database will reject the second insert.
Optimistic Concurrency
In an environment where multiple operators might be scanning packages simultaneously, concurrency control matters. The system uses optimistic concurrency with a RowVersion column on the box entity. When two operators try to modify the same box at the same time, the second save detects that the row has changed since it was read and returns a conflict error.
[Timestamp]
public byte[] RowVersion { get; set; }
The application catches the DbUpdateConcurrencyException and prompts the operator to refresh and try again. This is preferable to pessimistic locking (which would block other operators) in a workflow where conflicts are infrequent but possible.
Roles and Permissions
The system defines three roles:
- Operator: Can create boxes, scan packages, and view their own activity
- Supervisor: Can all operator functions, plus override scans, reopen completed boxes, and handle exceptions
- Administrator: Can manage users, configure system settings, and view all audit logs
Certain operations require explicit supervisor approval. For example, removing a package from a completed box is not something a regular operator should do. When a supervisor performs such an action, the system requires them to provide a reason, which is recorded in the audit log.
The Audit Trail
Every significant action creates an immutable audit log entry: box creation, package scan, package removal, status change, supervisor override, and user login. Each entry includes the action, the user who performed it, the timestamp, and any relevant context (such as the reason provided for a supervisor override).
The audit log is append-only. Entries cannot be edited or deleted, even by administrators. This is enforced at the database level: the application has no delete endpoint for audit records.
public class AuditEntry
{
public int Id { get; set; }
public string Action { get; set; }
public string UserId { get; set; }
public string EntityType { get; set; }
public string EntityId { get; set; }
public string? Reason { get; set; }
public DateTime Timestamp { get; set; }
public string? Details { get; set; }
}
The audit trail serves two purposes: operational (understanding what happened when something goes wrong) and compliance (proving that processes were followed).
Entity Framework Core and SQL Server
The application uses Entity Framework Core with SQL Server. EF Core's change tracking and migration system made it straightforward to evolve the schema as requirements clarified. SQL Server was chosen because it was already available in the deployment environment and its support for RowVersion (timestamp) columns simplifies optimistic concurrency.
The data model is deliberately normalized. Boxes, packages, users, and audit entries are separate tables with clear foreign key relationships. This makes queries straightforward and ensures that the audit trail does not depend on denormalized data that could become inconsistent.
Testing Business Rules
The most important tests are the ones that verify business rules:
- A package assigned to box A cannot be scanned into box B
- A completed box cannot accept new packages
- A supervisor override requires a reason
- An operator cannot delete audit entries
These tests use an in-memory database for speed, with seed data that creates realistic scenarios. The tests do not test EF Core itself; they test the business logic that sits on top of it.
What I Would Do Differently
The first version deliberately excluded several features: web-based dashboards, real-time updates across multiple terminals, and integration with external warehouse management systems. These are all valuable, but adding them before the core workflow was solid would have created a system that did many things poorly instead of one thing well.
The biggest lesson from this project is that industrial software has different priorities than consumer software. A flashy UI matters less than predictable behavior. A feature that works 95% of the time is not acceptable when the other 5% means lost traceability. And an audit trail is not a nice-to-have; it is the reason the system exists.