~/About~/Systems~/Foundry~/Blueprint~/Journal~/Projects
Book a Call
Blueprint

Freight Capacity Auction Clearing Engine

·7 min read·Kingsley Onoh·View on GitHub

Project

Freight Capacity Auction Clearing Engine

Proof type

Architecture proof

Best for

CTO / architect

Source

GitHub available

Inspect

Architectural Brief: Freight Capacity Auction Clearing Engine

A freight award can satisfy an optimization model and still be unsafe to publish. This system separates calculation, evidence, approval, and export so no single component can turn a solver response into an operating commitment by itself.

System Topology

Architecture diagramScroll on small screens

The web process owns request handling and tenant authorization. Redis carries bounded work and progress, while PostgreSQL owns every durable auction, bid, decision, approval, and export fact. The worker may ask a solver for an answer, but the answer becomes canonical only after the system records the selected backend, version, input hash, output hash, decisions, and terminal evidence.

Operational contract

The shipped clearing path accepts tenant-owned loads, carriers, lanes, policies, and sealed bids through the web interface, API, CSV, JSON, or Parquet import boundary. Import rows pass through staging first. Unknown columns, invalid numeric values, suspended carriers, and other unsafe rows remain quarantined instead of entering auction state.

Production clearing supports single_round_spot. The database schema names other modes because replay and later operating modes share the same domain, but the worker rejects unsupported production semantics. That is a deliberate boundary. A mode name in a schema is not proof that its clearing rules exist.

The output is more than an award row. A completed job records awarded, rejected, and unassigned decisions; policy and input snapshots; solver identity; input and output hashes; and any ranked relaxation suggestions. Suggestions stay inert until an operator changes policy. If approval is required, export remains blocked until the approval transition is durable. Export then renders from a frozen snapshot rather than rereading mutable auction state.

Infrastructure Decisions

  • Compute: Two OCaml processes, one Dream server and one worker. I chose separate processes over clearing inside the request cycle because solver deadlines, retries, cancellation, and process cleanup must not hold an HTTP request open or blur job state.
  • Canonical data: PostgreSQL 16 across 23 migration-defined tables, including the migration ledger. I chose it over DuckDB for live state because bids, awards, approvals, audit events, and export records need tenant-scoped transactions and relational constraints.
  • Job transport: Redis 7 with bounded FIFO queues, owned expiring locks, and capped progress streams. I chose it over PostgreSQL polling for transient job coordination because queue admission and progress are distinct from canonical freight facts. PostgreSQL still decides whether a job or award exists.
  • Solver: MiniZinc behind a shell-free child-process boundary. I chose an external solver process over an in-process optimization library because the executable, version, timeout, input, output, and terminal status need an inspectable evidence chain. A configured OR-Tools adapter is reported independently and never becomes a silent fallback.
  • Replay store: DuckDB and Parquet for read-only historical comparison. I chose this over loading replay scans into the transactional pool because large analytical reads have a different access pattern and must not compete with active auctions.
  • Interface: Dream with server-rendered HTML, HTMX, and a small browser module. I chose this over a separate single-page application because the operator surface can share authorization and error contracts with the API without another deployment unit.

Constraints That Shaped the Design

  • Tenant isolation: Tenant identity is an immutable request and job input. Carrier viewers require a carrier scope, and every canonical query is tenant-scoped. Request IDs correlate work but grant no authority.
  • Sealed bids: Carrier-facing reports filter to the caller's own carrier. Competitor bid values do not cross that boundary.
  • Solver distrust: Exit code 0 is insufficient. The parser requires one terminal solver status, rejects a second terminal status, and rejects any parsed solver record after terminal evidence. Separators and unrecognized lines are tolerated, but they do not count as evidence. Missing evidence ends as infeasible or failed, never as an award.
  • Approval authority: Proposed awards that require review remain non-exportable. Approval and rejection are explicit state transitions with a recorded decider.
  • Import uncertainty: Runtime configuration defaults MAX_CSV_UPLOAD_MB to 50, but this release doesn't claim the import handler enforces it. Schema drift and fatal row errors quarantine a run; duplicate rows can be skipped with an explicit warning.
  • Optional services: Notification, workflow, and webhook adapters default to disabled. Their health cannot change core readiness, and their outbox rows cannot mutate auction truth.
  • Observed scale: Local release proof covered a 432-row Parquet fixture, a two-bid award lifecycle, and accessibility checks across 19 real routes. The PRD's larger bid, load, and replay targets remain targets, not measurements.

Evidence boundary and scale limits

The release gate ran the OCaml unit suite, PostgreSQL 16 integration tests, Redis 7 integration tests, Dream lifecycle checks, hostile solver-process fixtures, DuckDB and Parquet fixtures, browser journeys, accessibility checks, and a packaged Docker image on 27 August 2026. A local lifecycle submitted two bids, persisted one award and one rejection, blocked export before approval, then returned a frozen export after approval.

That evidence proves the local control path.

It doesn't prove production throughput, live tenant behavior, managed deployment, collector delivery, or external adapter delivery. The current PostgreSQL pool defaults to 10 connections per process and must be budgeted before replicas are added. Redis uses one process connection with serialized I/O, so worker concurrency will eventually require measured queue and connection changes. Replay has a configured row budget, but that budget is a guard, not a benchmark.

The first scaling decision should follow measurement. If active clearing jobs create queue contention, add worker capacity while preserving owned locks and idempotent persistence. If transactional traffic becomes the limit, tune pool budgets and query plans before changing storage. If replay scans pressure shared disks, move analytical artifacts without moving canonical state. None of those changes should weaken the authority chain from tenant context to solver evidence, approval, and frozen export.

Decision Log

Decision Alternative rejected Why
Require one terminal solver status Trust process exit code 0 A child process can exit cleanly with missing, duplicated, or malformed domain output.
Persist ranked relaxation suggestions only Apply solver relaxations automatically Capacity or service exceptions are policy decisions. The solver may explain an infeasible case but cannot rewrite policy.
Freeze export snapshots Render every download from current rows Later policy, carrier, or auction edits must not change the record an operator previously approved.
Keep carrier visibility in report construction Redact a full operator report at the HTTP edge Building the narrow view first reduces the chance that competitor bid data enters a carrier response.
Fail startup on invalid configuration Clamp invalid ratios and timeouts Silent correction would make the running policy differ from the operator's supplied policy.
Keep integration delivery outside canonical state Let adapter success drive auction transitions Optional network services can be slow or unavailable. Auction truth must remain local and transactional.
#ocaml#postgresql#minizinc#optimization#freight-tech

Continue reading

2 companion pieces · all shown

See the rest of the Freight Capacity Auction Clearing Engine story.

You're reading Act IIBlueprint. Every other piece in this series is listed below.

Explore further

Put this system in context.

Go deeper into Freight Capacity Auction Clearing Engine, browse the surrounding system landscape, or discuss a build with Klevar.

Get Notified

New system breakdown? You'll know first.