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

Built· contract check, AI workflow, Kafka event driven and Kafka streams, Amazon S3 StoragePlanned· expand to other countries, delivery promise check, CO₂ emissions report
Meridian

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.

GitHubJava 21Spring BootSpring SecuritySpring KafkaReactReact RouterTypeScriptViteApache PDFBoxJUnitAssertJMockMvcArchUnitVitestTesting LibraryPlaywrightOllamaKafkaAWS SDK for JavaAWS S3LocalStackMicrometerPrometheusGrafanaKafka UIKafka ExporterDocker ComposeGradleGitHub Actions

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.

Courier options for one parcel with prices, delivery times and sources

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.

A contract blocked by three violations of German freight law

03

Parcel events on Kafka

Courier tracking events flow through three partitions keyed by parcel. Broken events go to a dead-letter topic so they never block the rest, and Grafana shows throughput, lag and dead letters.

Grafana dashboard with events processed, dead letters and messages per second

04

Private contract storage

Each business sees only its own contracts in S3. Meridian's internal team can read them for support, and every look is written to the audit log.

Who can see a business's contract files

AI workflow automation

  1. 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.

  2. 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.

  3. 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 setReaderFully correctRule recallTraps turned into rules
14 contracts, usual wordingRules14 of 1415 of 150
14 contracts, usual wordingRules + model14 of 1415 of 150
10 contracts, unusual wording and trapsRules4 of 100 of 60
10 contracts, unusual wording and trapsRules + model7 of 104 of 60

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.

How rules, the model, the guard and a person handle three contract clauses

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+
PatternAt tyntecIn Meridian
Routing configurationAn 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 pipelinesMillions 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 commitOperators 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 S3A 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.