Mercala
An online store you run by talking to it.
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.
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 providersThe core: identity, catalog, cart, order, payment, inventory, media, and a platform package holding multi-tenancy, security, the outbox and idempotency.
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.
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.
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.
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.
Onto an AWS spot instance, with no access keys anywhere: CI assumes a role through GitHub OIDC, the host uses an instance profile.
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.
| Piece | Count | Note |
|---|---|---|
| Java files | 297 | 212 main, 85 test, ~26,000 lines |
| Tables | 17 | 20 Flyway migrations, schema owned by Flyway |
| HTTP endpoints | 49 | across 20 controllers |
| Tests | 388 | including the tenant-bypass test |
| AG-UI event types | 16 | the agent streams a typed protocol, not raw text |
| Image providers | 5 | behind 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.