Case study · My tyntec work, in a new domain
Meridian
A shipping platform for German online shops: real courier prices, courier contracts checked against German law, and routing rule changes with a test run and four-eyes approval.
2026Solo projectFull-stack software engineeringAI engineering

About the project
Meridian sits between an online shop and its couriers. It compares DHL, Hermes, DPD and GLS for every parcel, checks courier contracts against German law, and changes routing rules only after a test run and a second approval.
Java 21 and Spring Boot keep pricing, contract checks and routing rules explicit and testable; React and TypeScript give staff a clear view of comparisons and proposed changes. Kafka carries parcel events, while private S3 storage keeps each business's contracts separate.
A local Qwen2.5 7B language model running through Ollama reads the contract clauses the rules cannot, and every answer is checked against the clause before a person sees it. Grafana shows what is happening.
What makes it unique
AI workflow automation, measured
I introduced a local language model for the clauses the rule engine could not read, kept both readers, and measured them on the same labelled contracts: the model raised recall on unusual wording from 0 of 6 to 4 of 6 with no wrong rule accepted.
Contract check against German law
Courier contracts are checked against HGB §§ 431, 438, 439 and 449 and PostG § 73 before they can route a parcel. I have not seen this in the shipping platforms I looked at.
Real data, with sources
Real couriers, real services and published prices, each linked to its source and the day it was checked. Sample data is labelled where it appears.
How it works
01
Compare couriers
For each parcel, Meridian applies every courier's own size and weight rules, picks the cheapest service that fits, and shows price and delivery time side by side. Every price links to where it comes from.

02
Contract check against German law
Upload a courier contract as PDF or Word. Liability, notice periods and the limitation period are checked against the HGB and the PostG, and a term that breaks the law blocks the draft.

AI workflow automation
- 01
The problem with rules alone
Every courier words routing differently: "Postleitzahlbereich 50", "the 20 postal area", "PLZ-Gebiet 8". My rule reader only understood the phrasings it had a pattern for, so every new contract meant another hand-written pattern. On contracts with unusual wording it found 0 of 6 routing rules.
- 02
Why an AI workflow
Reading varied language is what language models do well; deciding what is allowed has to stay exact. So I split the job. A local model (no data leaves the machine, no API costs) reads the clauses the rules cannot, and deterministic checks and people decide.
- 03
What it solved
On the same unusual contracts, rules plus the model found 4 of 6 rules instead of 0, every accepted rule was correct, and no trap became a rule. New wording no longer needs a new pattern in the code.
| Labelled set | Reader | Fully correct | Rule recall | Traps turned into rules |
|---|---|---|---|---|
| 14 contracts, usual wording | Rules | 14 of 14 | 15 of 15 | 0 |
| 14 contracts, usual wording | Rules + model | 14 of 14 | 15 of 15 | 0 |
| 10 contracts, unusual wording and traps | Rules | 4 of 10 | 0 of 6 | 0 |
| 10 contracts, unusual wording and traps | Rules + model | 7 of 10 | 4 of 6 | 0 |
In one run, the model found 4 of 6 expected rules. All 4 proposed rules were correct; it invented none. Results may vary between local runs, even at temperature 0.

Built on my production work at tyntec
At tyntec I build and run fullstack backend services. Meridian carries those patterns into a new domain. The figures here are from tyntec, not from Meridian, and client, product and data details are left out on purpose.
- a manual reconciliation automated with an AWS S3-backed service
- 48 h → < 1 h
- return on investment of that automation
- 1,300 %
- of messages a day through Kafka pipelines
- Millions
- countries in scheduled, distributed CSV ingestion
- 50+
| Pattern | At tyntec | In Meridian |
|---|---|---|
| Routing configuration | An operator tool for routing rules, kept apart from the logic that applies them, with every change audited and restorable. | Routing rules as versioned data, filled from courier contracts by the rules and the AI workflow. |
| Kafka pipelines | Millions of messages a day: idempotent consumers, ordering per customer, bad events in quarantine. | Courier events on three partitions keyed by parcel, with a dead-letter topic, watched in Grafana. |
| Review before commit | Operators see exactly what will be written before anything is saved. | A test run on recent parcels and a second approver before any rule goes live. |
| Automation with AWS S3 | A manual reconciliation of up to 48 hours now takes under one hour. | Private contract storage per business in S3, with every look by the internal team audited. |
Challenges and learning
Challenge: In my first Kafka load test, two of the three partitions stopped with a lag of about 400 events. Spring Kafka's default dead-letter topic had been created with one partition, so broken events from partitions 1 and 2 could not be parked, and every healthy event behind them waited.
Solution: I named the dead-letter topic explicitly and gave it three partitions, so a broken event keeps its partition, and added a test with an embedded Kafka broker for exactly that path. The next run of 16,493 events drained to a lag of 0, with every event accounted for.
Learned: The same lesson I learned in production at tyntec: one bad message must never block the healthy ones. Defaults are decisions too, so the dead-letter path needs its own test.
Technologies used
Backend ·Java 21
- The domain as plain records and classes: contract reader, German law check, routing rules, rule changes, parcel lifecycle.
- No framework imports in the domain; architecture tests fail the build if one appears.
Backend ·Spring Boot
- The REST API, request validation and errors in the problem details format (RFC 9457).
Security ·Spring Security
- Two separate API areas: a public one for comparison and the contract check, and a staff console with operator, approver and viewer roles.
Documents ·Apache PDFBox
- Extracts text from uploaded PDF contracts; the app also reads DOCX, ODT and plain text using Java ZIP and XML APIs.
Testing ·JUnit, AssertJ and MockMvc
- 93% test coverage across individual freight-law rules and API workflows, including drafting a rule change, running a test, submitting it and approving it.
Testing ·ArchUnit
- Keeps the domain free of Spring and Jackson, and the application layer free of adapters.
Testing ·Contract-reading evaluation
- 14 labelled contracts with traps, including hidden instructions and conditional clauses, read on every build with precision and recall per run.
AI ·Ollama with qwen2.5 7B
- A local language model that reads the contract clauses the rule reader cannot, answering in a fixed JSON schema.
- Every answer passes a guard that checks it against the clause, then the same law check, test run and approval as any other rule.
- Compared with the rules alone on the same labelled contracts.
Storage ·AWS S3 with LocalStack
- Uploaded contracts stored through the AWS SDK for Java with server-side encryption; LocalStack in Docker locally, AWS S3 by configuration.
- Private storage per business: no business can see another's files, and every read by the internal team is written to the audit log.
Frontend ·React, TypeScript, React Router and Vite
- The web app: home, courier comparison, contract check, contract library, rule changes and checkout preview.
Testing ·Vitest and Testing Library
- Component tests for the delivery choice, the contract result and the rule-change test run.
Testing ·Playwright
- End-to-end browser tests exercise the web app and API together: courier comparison, contract checks and uploads, rule-change scenarios, checkout, and phone-width layouts. They run in GitHub Actions.
Build ·Gradle and GitHub Actions
- One pipeline that runs the backend tests, the type check, the web tests, the web build and the browser tests.
Local infrastructure ·Docker Compose
- Starts the local S3 service, Kafka, Kafka UI, Prometheus, Grafana and Kafka exporter.
Data ·PostgreSQL and FlywayPlanned
- Replace the in-memory storage behind the existing ports.
Security ·KeycloakPlanned
- Login for the console.
Messaging ·Apache Kafka and Spring Kafka
- Courier tracking events on a topic with three partitions, keyed by parcel so each parcel's events stay in order.
- Three consumer threads, retries with back-off, and a dead-letter topic that keeps each failed event's key and partition, so one bad event never blocks the rest.
- A courier simulator sends bursts with duplicates, swapped steps, broken JSON and unknown parcels; an embedded-broker test proves the dead-letter path.
Observability ·Micrometer, Prometheus, Grafana, Kafka UI and Kafka Exporter
- Micrometer exposes event outcomes, dead letters, processing time and the review queue; Prometheus scrapes these and Kafka Exporter adds consumer lag per partition.
- A provisioned Grafana dashboard and Kafka UI show events, topics, messages and consumer groups.
Want to know more?
I'm happy to walk you through the product and engineering decisions on a call. Let's connect.

