Skip to content
CK/SYSTEMS
Project: laptop-loan-management-system Source: Retrospective active

Laptop Loan Management System: engineering a practical asset-tracking platform

How I built and hardened a laptop loan management system through iterative releases, balancing inventory accuracy, policy checks, and operational reliability.

journal engineering laptop-loan asset-management system-design

I design state-heavy systems around invariants because silent data inconsistency is the most expensive kind of bug.

I started this system as a small inventory tracker and quickly discovered that inventory is not a CRUD problem; it is a state problem. Once other teams started relying on it, “good enough” logic began producing bad answers.

Context

The earliest version of the system focused on recording who had what laptop when. In practice, the shared workload exposed concurrency, policy drift, and reporting failures:

  • Multiple support agents could act on the same device without a single source of truth.
  • Late-return and renewal business rules were applied in different places and behaved differently.
  • Incident response required reconstructing a timeline from mutable state instead of immutable history.

The result was a classic reliability gap: the UI looked fine most of the time, but the domain behavior did not hold over time.

Forces

  • Speed vs correctness: fast delivery was needed, but permissive assumptions created correctness debt that surfaced as reconciliation incidents.
  • Policy flexibility vs consistency: the domain needed room for exceptions, while operationally we could not tolerate silent bypasses.
  • Local state simplicity vs operational auditability: lightweight records were easy to build, but insufficient for tracing what happened at 2:00 AM during an outage.
  • Single-screen correctness vs end-to-end guarantees: frontend validation alone could not prevent invalid transitions at API/service boundaries.

Architecture Decisions

  • Model the loan as a constrained lifecycle rather than loose flags.
    Represented state transitions explicitly (available -> checked_out -> returned) with guards and invalid-state rejection.
  • Centralized policy enforcement at command boundaries.
    Consolidated eligibility checks (late return grace periods, renewal windows, role permissions) into one validation path to avoid rule duplication.
  • Made loan mutations atomic and idempotent.
    Repeated submissions now resolve to the same logical outcome and prevent duplicate active loans under race conditions.
  • Implemented immutable transition history.
    Persisted lifecycle events separately from current state so investigation is based on facts, not inference.
  • Standardized terminology across UI, services, and operations.
    Shared transition vocabulary reduced interpretation errors during handover and incident work.

Consequences

  • Stronger invariants: overlapping active loans became structurally impossible in normal operation.
  • Better observability: each critical transition is explainable and attributable.
  • Clearer incidents: fewer “who changed what” disputes because policy failures are surfaced at the same boundary.
  • Higher implementation cost on write paths: extra validation and transaction boundaries increased complexity and response latency under heavy write load.
  • More maintainable onboarding: future contributors can inspect transition rules before learning every UI edge case.

Tradeoffs

  • Consistency over latency: users may wait slightly longer for validation feedback, but we avoid silent data corruption.
  • Safety over ad-hoc flexibility: policy changes require deliberate updates in one place, which slows one-off hacks.
  • Durability over convenience: immutable logs increase storage and analysis needs, but significantly reduce ambiguity.
  • Domain clarity over feature volume: fewer unbounded feature combinations, more deliberate workflows.

Risks

  • If another integration writes state directly, the model’s guarantees can be bypassed.
  • Retention gaps in transition logs would weaken post-incident reconstruction.
  • Alert fatigue around policy failures could hide meaningful anomalies.
  • Insufficient training on transition states may encourage operators to rely on assumptions instead of system messages.

Next Steps

  • Add explicit tests for invariants (e.g., one open loan per device) and randomised concurrent transition scenarios.
  • Split policy validation into composable modules so policy evolution remains explicit without coupling all rules together.
  • Introduce a dedicated notification workflow driven by domain events to decouple user-facing writes from side effects.
  • Add SLOs and dashboards around rejected transitions to detect policy churn early.

Selected commits

  • e297a0c — feature: add basic device and loan model with automated tests
  • 509545a — chore: enhance sample DB seeding and deterministic local QA data
  • 7e12dfd — feature: implement per-tenant routing boundaries

Fagan inspection: design review by commit evidence

I run each retrospective article through a Fagan-style inspection checklist so claims are supported by history, not vibes.

Inspection scope

  • Inputs: linked commits in this article, architecture claims in prose, and explicit design trade-offs.
  • Objective: separate intentional architecture from incidental implementation details.
  • Exit condition: no major claim remains unlinked to a commit trail or clear constraint.

What I inspected

  1. Problem framing — was the failure mode explicit and specific?
  2. Decision rationale — was the reason for each structural choice clear?
  3. Contract boundaries — are state transitions, validation, and permissions explicit?
  4. Verification posture — are risks paired with tests, gates, or operational safeguards?
  5. Residual risk — what is still uncertain and where is next evidence needed?

Findings

  • Pass condition: each design direction is defensible as a trade-off, not preference.
  • Pass condition: at least one linked commit backs every architectural claim.
  • Pass condition: failure modes are named with mitigation decisions.
  • Risk condition: any unsupported claim becomes a follow-up inspection item.

How I design things (Fagan-oriented)

  • Start with a concrete failure, not a feature idea.
  • Define invariants before interface details.
  • Make state and lifecycle transitions explicit.
  • Keep observability at decision points, not only at failures.
  • Treat governance as a design constraint, not a post hoc process.

Next design action

  • Turn this inspection into a backlog trail: each remaining risk maps to one upcoming commit with acceptance evidence.
Newsletter

Short notes on building AI agents in production.

One email when something worth sharing ships. No fluff, no daily cadence, no recycled growth-thread noise.

Primary use: consulting updates, governed AI workflow lessons, and major project writeups.

Newsletter

Short notes on building AI agents in production.

One email when something worth sharing ships. No fluff, no daily cadence, no recycled growth-thread noise.

Primary use: consulting updates, governed AI workflow lessons, and major project writeups.