A freight marketplace and a yard management system - two pieces of logistics software we built for one operator. One had to be taken apart while it ran: the marketplace existed, but it sat on a slow monolith. The other did not exist at all: the yard was run entirely on paper and spreadsheets.
- Industry: road freight. Grain and grain products, dry food bulk, vegetable oils, and liquid bulk.
- Client: a freight and logistics operator with its own fleet, its own service shop, and a network of contracted carriers. The location has been generalized to Eastern Europe.
- Engagement: 2 years and 5 months, first production release in 4-6 months, with a cross-functional team of 5-8 specialists.
- Handover: handed over to the client’s in-house engineering team.
- Stack: PHP 8, Laravel, Vue.js, Go, PostgreSQL, Redis, RabbitMQ, WebSockets, Kubernetes.
This project is presented in a generalized form. Some details and identifying elements are intentionally omitted due to confidentiality requirements.
Results at a Glance
- The load search page went from 8-12 seconds to under 1 second. The slow monolith was the top complaint from both sides of the marketplace.
- Invoicing moved from weeks to days. Before, an invoice waited on original documents arriving by mail.
- 10 manual steps in moving a truck through a facility were tied into one digital process.
- 3,000-5,000 carriers and 400-600 shippers on the platform at handover.
- Around 3,000 loads a month moved through it.
- 8 business domains, 8 roles, roughly 20 entities in the operational model of freight haulage.
- Still in production and still evolving more than three years after handover to the client’s team.
A Slow Monolith and a Yard Run on Paper
We joined a company that was already hauling freight, and we took on two problems of opposite character. Both tracks ran in parallel.
The freight marketplace existed, effectively a private load board for the operator’s carrier network. But it was a single PHP monolith with an inconsistent, hard-to-maintain API, and it was slow. The load search page took 8-12 seconds to open. Both sides of the market complained: carriers hunting for freight and shippers posting it. We could not switch the system off, because real shipments were moving through it.
There was no yard management system at all. Everything that happened to a truck on site was tracked by hand, on paper and in spreadsheets.
Product One: Freight Marketplace and Carrier Portal
Shippers post freight, carriers find loads and take them into work. Each side has its own portal, and carriers also have a mobile app. At handover the platform carried 3,000-5,000 carriers and 400-600 shippers, with around 3,000 loads a month moving through it.
What We Did to the System Itself
We broke the monolith apart along business functions, split the frontend from the backend, moved pieces into separate services, and put a consistent REST API between them in place of the old one. Background work went into queues, real-time traffic was separated from ordinary HTTP flows, and the mobile client got its own API.
The search page was slow for several reasons at once. It assembled results inside the monolith: for every load it separately pulled the shipper’s data, terms, and status, some filters ran on unindexed columns, and document processing and reports were writing to the same database at the same moments. We moved search into its own service with its own read table, updated from RabbitMQ events when a load is posted, changed, or taken. Composite indexes went in for the filter combinations people actually used, and reference data and frequent queries went into Redis. Load search now opens in under a second instead of 8-12.
That came at a cost. Search results lag behind the main database by the time it takes to deliver an event, so a new load does not appear instantly. And when a carrier takes a load, its status is checked against the main database rather than the search table; otherwise two carriers could take the same load.
Support Inside the Product
Carriers message support directly from the portal. The chat is integrated with WhatsApp and the phone system, and an incoming message routes automatically to the account manager assigned to that client.
What Changed for the Carrier
Finding out available lanes, a rate, or the status of an order used to mean knowing which manager to call. Now lanes and rates are visible in the portal, and a question goes into one chat and reaches the right manager.
Electronic Document Flow: Paperwork That Stopped Disappearing
The most underrated part of the project, and one of the parts with the clearest financial impact.
A shipper and a carrier do not exchange one document per load. They exchange a chain of three to five, and half of that chain is physical: sign electronically, attach scans, mail the originals, then wait for confirmation that the originals arrived.
Until the originals arrived, no invoice could go out. Two to four weeks passed between delivery and invoice, and for all that time the money sat frozen in the operating cycle. Each stage existed only in an email thread and in somebody’s memory, and the question “do you have the documents yet?” was answered by phone.
We recorded the full path of every document in the system:
- received;
- signed;
- scans attached;
- originals mailed;
- originals received.
Correspondence about a document is attached to that document. At any moment you can see which stage every piece of paper on every load is at, without a single phone call. Invoicing became a matter of days instead of weeks.
Shipment Tracking as Recorded Events
Alongside the documents we captured the facts of the haul itself: when the freight was picked up, which route it took, how long loading took, and which documents the freight was released against. Proof of delivery became a record in the system rather than a phone call.
A shipment stopped being “somewhere out there” and became a set of timestamped events. You can lean on those in a dispute with a counterparty, and in working out where the hours actually go.
How the Yard Worked Before Automation
Moving one truck through took ten steps. The driver handed documents to the gate operator. The operator searched for the order by hand or phoned a logistics coordinator to confirm it. Truck, driver, and cargo details went into a logbook or a spreadsheet. The coordinator decided where to send the truck. The driver waited. After each checkpoint, status was passed to the next employee by voice. Any change meant another phone call. At the exit, everything was recorded again.
Three or four people took part in the cycle, and none of them saw the whole picture. The queue did not exist as an object - it existed in people’s heads.
Product Two: Dispatch Console, Dock Appointment Scheduling, and a Live Queue
A dispatcher workstation that had not existed in any form before us. It runs across several facilities, each handling 15-25 trucks a day.
- Dock appointment scheduling. Trucks book a specific slot days in advance instead of showing up whenever they arrive. The dispatcher sees a live queue: which truck is behind which, and on which day.
- A card on click. Clicking a slot opens everything needed to make a decision: plate, driver, vehicle make, phone numbers, and the documents for that load.
- Where the truck is right now. The system shows which of the four to six on-site checkpoints a truck has already cleared and which one it is at. Status is no longer passed along by voice.
- The system enforces checkpoint order. A truck cannot move to the next checkpoint while skipping the previous one. That used to depend on people paying attention; now it is a rule inside the process.
- Permissions at each checkpoint. Every checkpoint has its own set of roles, its own documents, and its own timestamp. Who can do what at a given point is configured, not assumed.
- Manual override when it is needed. The dispatcher can reorder trucks in the queue. A real yard does not always follow the plan, and the system allows for that instead of blocking work.
Roles, ERP Integration, and the Data Model
Eight business domains: freight requests and loads; the carrier network (carriers, drivers, vehicles); shipment tracking; documents; gates and checkpoints; time slots and the queue; support and communication; ERP and settlements.
Eight user roles with access policies: gate operator, dispatcher, shipper, carrier, driver, support manager, accounting, and administrator.
Integration with the client’s ERP and electronic document flow, so that documents and money do not live apart from the shipment.
Roughly twenty entities in the shared data model: company, counterparty, shipper, carrier, driver, vehicle, freight, freight request, load, facility, gate, checkpoint, stage, event, document, file, chat, message, and assigned manager.
Decomposing the Monolith Without Stopping Freight
The system could not be switched off for a single day. So we did not rewrite it. We took it apart in slices while it ran: isolate a business function, move it into its own service, fix the API contract between the old and the new side, shift traffic, move on. During migration the old flows ran in parallel with the new ones, so that no shipment stopped because of our release.
The architecture formed evolutionarily: as stable business boundaries emerged, we moved functions out of the monolith into separate services. What came together in the end looked like this:
- microservices separated by business workflow;
- PostgreSQL as the source of truth for loads, trips, and documents;
- Redis for reference data, rates, and frequent search queries, invalidated on change events. If Redis is unavailable, requests fall back to PostgreSQL: slower, but working;
- RabbitMQ for everything that should not keep a user waiting: ERP sync, document processing, notifications, and search updates. Retries were there from the start; idempotency for the ERP exchange came later, which is why it shows up under what we would do differently;
- WebSockets for the dispatcher’s live queue, truck statuses at checkpoints, and support chat;
- Go for the client support chat, as a separate service. Business logic stayed in PHP/Laravel, which suited the CRUD and integration work; the chat, with its long-lived connections and WhatsApp and phone integration, was simpler to run on its own;
- S3-compatible object storage;
- Kubernetes, GitLab CI/CD;
- Prometheus, Grafana, and centralized logging.
What We Would Do Differently
We would fix the context boundaries earlier and agree on API contracts before implementing services. We would introduce a single event versioning scheme between services, and idempotency for integration operations, from the start. Correlation IDs and observability would go in by the first release, not after. And we would write more contract tests around the ERP and the queues, where mistakes cost the most.
Data and Access Control
The system handles personal data of drivers and staff, transport and contract documents, routes, and commercial terms.
- access separated by a role model and policies;
- authentication on JWT and OAuth2;
- authenticated service-to-service communication;
- files stored apart from application logic;
- centralized logging of user actions.
The project held no formal certification against international standards.
Outcome and Handover
The platform became part of the company’s day-to-day operations. Once the product reached stable production, we handed further development to the client’s in-house engineering team. It is still running and still evolving more than three years later.
Today the carrier network counts tens of thousands of vehicles.
We covered a related logistics problem in our freight procurement platform case study, and how systems are maintained after launch is described under technical support.
Interfaces have been recreated for publication. Production screens cannot be shown due to an NDA.