Our app once let two carers both record the same 9 pm tablet as given. One typed 21:00, the other typed 21:15, and the app saw two different doses.
That bug is the reason this post exists. I built Care Circle for Graph Hacks 2026 (Track 02, agent memory and coordination). It is a shared medicine record for families who look after one person together. This post is about five things that went wrong while building it, and what each one taught me. They look unrelated. Four of them are the same mistake.
No flag is not the same as safe. When the data has a hole, the code must say so. If it fills the hole with something that looks fine, the hole becomes an answer.
The problem
It is 9 pm. Ravi changes his mother's warfarin dose by phone. His sister Meena is on the night shift and has not seen the change. Tomorrow a pharmacist adds an antibiotic that interacts with warfarin. Everyone is doing the right thing, and still nobody has the whole picture.
Care Circle gives the family one shared list. Anyone can add a medicine by photo, voice note, PDF or typed text, on the web or in Telegram. Everyone can then see what is on record, who has seen each change, which medicines are listed as interacting in public sources, and which of today's doses are already given.
It records and cross-checks. It never tells anyone what to take, stop or change. A scanner rejects any outgoing text that does.
Why a graph
Every question I wanted to ask was about a relationship:
- Do these two medicines interact? A line between two medicines.
- Do they interact through a shared liver enzyme? A path of two steps: one medicine blocks an enzyme, the other depends on it.
- Who has not seen this change? A line that is missing between a carer and an entry.
- Is Telma-AM (telmisartan with amlodipine) the same thing as the plain amlodipine a second doctor prescribed? A brand contains an ingredient, and the ingredient is a drug.
So I put everything in FalkorDB, as three kinds of graph. One shared graph holds public data: about 218,000 interaction pairs from DDInter and the FDA's table of liver enzymes. Each family gets its own graph, so a query can only ever see that family's data. A small control graph holds accounts and access keys, and no health data.
A flag is a join between a drug in the family's graph and the same drug in the shared graph. Each flag shows the source and the path that produced it.
Bug 1: two carers, one tablet
The first version of the double-dose guard matched what carers typed. A dose at 21:00 and a dose at 21:15 were different, so both were recorded.
The fix has two parts. Doses are now marked against the slots of the confirmed schedule, so a time someone typed is moved to its slot. And the claim for a slot is one query: it creates a node keyed by person, drug and slot, and it marks the claim as won only if it created that node. FalkorDB runs one write at a time on a graph, so two carers cannot both create it.
I tested it with 100 rounds of 10 simultaneous claims on one slot. There was exactly one winner in every round. That is one test on one machine. It shows the claim is atomic in the database. It is not a proof.
Bug 2: one tablet, two rows
My first design stored one entry per ingredient. Telma-AM then showed up on the checklist as two rows with two ticks, although it is one tablet. A reviewer also found that the same ingredient from two products at the same time slipped past the guard.
The fix was to make the tablet a node. A product node points to each ingredient, and each dose points to its product. The checklist shows one row and one tick, and each ingredient is still checked for interactions on its own. Finding a duplicate became one query: group a person's current entries by the drug they belong to, and keep the groups of two or more.
Bug 3: the app did not know who you were
This one I found while recording the demo video.
In a demo circle, the app did not recognise which carer you were. The carers' ids in the database carry a suffix, like ravi-3f9a1c2e. The browser knew them by the plain name ravi. The Circle screen compared the two, they were never equal, and so there was no "(you)" tag and no "I have seen this" button. The feature the whole demo is about was missing in the demo.
The fix is small, and it is already live. I added a browser test that starts a demo circle, checks that Ravi is marked as "you", presses his button, switches to Meena and checks that the marker moves.
Look at the shape of it. A value that did not match was treated as "no match", and "no match" meant "show nothing". Nothing failed loudly.
Bug 4: the database crashed, and I caused it
On 17 October at 08:13 UTC the live site began answering with a 502 error. The database was restarting in a loop. It loaded its snapshot, and about a second and a half later it crashed while replaying its append-only file. I restored the site from the snapshot.
I took the 644 commands for the crashing graph and cut them down to one. A single GRAPH.EFFECT command addressed to a graph that does not exist is enough to abort FalkorDB 4.22.1. In real use it looks like this: write to a graph, delete the graph, write to the same name again. The log from that sequence crashes the server when it is replayed. A clean error would have been fine.
The cause was a load test I had pointed at the live server. It created and deleted throwaway graphs, and that produced exactly such a log. So this was my own mistake, not an attack.
What changed:
- Production now keeps snapshots only and has no append-only file, so there is nothing to replay.
- A backup is taken before every deploy, and again every six hours.
- The stress script now refuses to run against any server that is not on my machine, unless I say the server is disposable.
The small reproduction is in docs/FALKORDB_CRASH.md.
Bug 5: the same query, two versions
Production runs FalkorDB 4.22.1. CI also runs 6.0.2. Three times a query worked on one and failed on the other.
- A loader query that used
UNWINDover a list of maps failed on 6.0.2 with "Variable i not found". -
EXISTS { }subqueries are not supported, so I rewrote that test with a set comparison. - On 4.22
algo.betweennessreturned 0, 1 or 2 for the middle node of the same three-node chain, on different runs. The other algorithms I used were stable. I now read the links from FalkorDB and compute betweenness exactly on the server, and I checked that code against the definition on 60 random graphs.
I now run the tests against both versions before every push.
The test I trust most
"Who has not seen this change?" is the kind of query that looks right and is slightly wrong. So I wrote the rule down separately from the code and compared the two on 40 generated circles.
The graph query matched the rule in 40 of 40. A weak baseline that only tracked each carer's latest change matched 4 of 40. When I broke the code on purpose, by letting a carer count their own entry as unseen, the result fell to 0 of 40 and the check failed.
I also built a fair baseline, two SQL tables and one window query. It matches 40 of 40 as well. So this test shows that the graph query is correct. It does not show that a table could not do it.
What the data does and does not say
26% of the DDInter pairs have no severity. Care Circle shows them as "unrated" and says that unrated is not the same as safe. That is the sentence at the top of this post, as a rule in the code.
The two-step enzyme path finds 61 of the 145 moderate or major pairs that DDInter lists between the same medicines. Of the pairs it flags, 82% are also listed in DDInter. So it is precise and narrow. I use it next to the direct lookup, never instead of it.
For agents too
For Track 02, the same graph is open to software agents over MCP and REST. An agent can write a note about a specific entry, and another agent in a new session can recall it with the path back to that entry. The note is labelled "noted by the agent, unverified", and it never changes what a person confirmed.
What I would tell someone building this
- Ask what your code does with "I don't know". A missing rating, a missing match, a missing time. Each one is a place where a default quietly becomes an answer.
- Test against a rule written down separately from the code. Then break the code on purpose, and check that the test notices.
- Do not run load tests against production. Make the script refuse to.
- Run your queries on more than one database version.
- Say how far a result goes. "One winner in 100 rounds" is not a proof, and I wrote it that way.
Live demo (press "Start a demo circle"): https://3-228-70-117.sslip.io
Code, queries and benchmarks: github.com/aniket-3001/CareCircle. The graph model and the queries the product depends on are in the README. Benchmarks are in docs/BENCHMARKS.md.
I built this with help from Claude (Anthropic) through Claude Code. I ran and reviewed the code and the tests myself.
Built for Graph Hacks 2026 (WeMakeDevs x FalkorDB), Track 02: Agent Memory and Coordination.

Top comments (0)