All projects Commerce · In build

Mercala

An online store you run by talking to it.

297Java files across four Maven modules, ~26,000 lines
388tests, and a bypass test that proves tenants cannot read each other
0Elasticsearch clusters — the search is Postgres
01Why it exists

Most of setting up a shop is typing things into boxes. Name, description, SKU, price, each size, a photograph for each colour. Mercala replaces that with a sentence, and asks only when something is missing.

The same inversion runs on the other side of the counter. Nobody searches for “navy linen shirt, regular fit” — they search for the occasion. So the shopper talks too, and the catalogue is read for meaning rather than matched on words.

It is built Java-first because the thing underneath is a ledger of other people’s money and stock, and that is a place where I want types, transactions and a schema somebody else can audit.

02The shape of it Four Maven modules: one monolith, two services carved out for extraction, one contracts jar.
Next.js webserver-first, bunmercala-core :8080modular monolithmercala-agent :8082spring ai toolsmercala-image-gen :80835 providersKafka3 topics, outboxPostgres / ParadeDBbm25 + pgvector + rlsS3 / MinIOproduct imagery

The core is a modular monolith — nine packages behind one deployable — because splitting on day one buys distributed transactions before it buys anything else. The agent and the image generator were carved out because they have genuinely different failure and scaling shapes: one waits on a model, the other waits on an image provider for up to three minutes.

Adding a provider means adding a class that implements the interface — never adding a branch to an existing one.

CLAUDE.md, on the five image providers
03The stack, and what each piece is doing
Java 21 · Spring Boot 3.3.5

The core: identity, catalog, cart, order, payment, inventory, media, and a platform package holding multi-tenancy, security, the outbox and idempotency.

Spring AI

Tool calling, not chat completion. Three tool beans — catalogue, imagery, and a human-in-the-loop set that can stop and ask the merchant a question mid-turn.

ParadeDB · pgvector

Hybrid search inside Postgres. BM25 for the words, a 1536-dimension vector index for the meaning, fused with reciprocal rank fusion. No second datastore to keep in sync.

Kafka · transactional outbox

Three topics. A product change writes its event in the same transaction as the row, and a relay publishes it after commit, so the event and the data cannot disagree.

Resilience4j

A circuit breaker per dependency, tuned per dependency — one image provider’s cold start legitimately takes three minutes, and the inherited five-second threshold was opening the circuit on a working backend.

Terraform · Ansible · OIDC

Onto an AWS spot instance, with no access keys anywhere: CI assumes a role through GitHub OIDC, the host uses an instance profile.

04Three layers of tenant isolation Each one alone is a single point of failure, so there are three.

A tenant should not be able to read another tenant’s rows even if I write the query wrong.

  • Role checks in codeFour roles, enforced at the method. This is the layer that fails first when someone adds an endpoint and forgets.
  • A Hibernate tenant filterA request-scoped tenant context applied as an aspect, so ordinary repository calls are scoped whether or not the caller remembered.
  • Row-level security in PostgresThirteen policies across nine migrations. This is the layer that holds when the other two are bypassed — and there is a test that issues a raw query to prove it.
  • The standing ruleNever weaken one layer without a test proving the others still hold.
05What is counted, and what is not The repository’s own roadmap is the source for both.
Read from the repository on 21 September 2026
PieceCountNote
Java files297212 main, 85 test, ~26,000 lines
Tables1720 Flyway migrations, schema owned by Flyway
HTTP endpoints49across 20 controllers
Tests388including the tenant-bypass test
AG-UI event types16the agent streams a typed protocol, not raw text
Image providers5behind one router with a fallback chain

What is not done, stated plainly: payment charging was never wired. The provider adapters and the inbound webhook both exist; the call between them does not, so an order means ordered, not paid. Metrics and tracing are also still open. These are in the repository’s roadmap as open items, and they are open here too.

The interesting part was not the AI.

It was making a shop that several merchants share impossible to leak across, and making search good enough without a second database to operate.