Teamwork · · 7 min read
The contract between three AI teams.
A front-end / back-end / AI division of labor that survived two iteration cycles without renegotiation. Notes from Goodle.
Written by Jiajun (Eddy) Huang
Goodle had six people on it: a PM, three backend engineers, one AI engineer, and me on the front end. When I talk about the project, people tend to assume the hard part was the model — getting a photo of a dog to come back as something that reads like a personality.
The model mattered, but that isn’t where the difficulty lived. It lived in one card on the screen: the personality profile that sits next to a pet’s photo. All three teams had a hand in that card. The AI side decided what it said. The backend decided how it was produced and stored. I decided how it appeared, and what the screen did while you waited for it. And each of us wanted the freedom to keep changing our part.
What made that workable wasn’t a clever prompt. It was a contract: a small, deliberately boring agreement about the shape of that card and the handful of routes that carried it. This note is about what was in it, why it held, and what I’d bring to the next team.
What we were building
Goodle is a pet adoption app. The problem it goes after is easy to state: adoption listings lean almost entirely on photos. People scroll, fall for a face, and only find out later whether the animal’s energy or play style actually fits their home. When it doesn’t, the shelter absorbs the cost.
Our bet was that one photo, plus an optional short video, could produce a personality summary structured enough to help with matching, while staying honest about what a model can actually infer from a few seconds of footage.
Under the hood, that’s a short pipeline. The photo is parsed into structured tags: breed, age band, posture, a few environmental cues. If there’s a five-to-fifteen-second clip, frames sampled from it are scored for energy, emotional stability, and play preference. Then Gemini turns those signals into a short, readable sketch. One decision mattered more than it looked at the time: the language model never sees the raw image. It only sees the features the earlier steps produced.
The result lands right next to the photo in a swipe-style browsing flow, so the personality shows up at the moment someone is deciding yes or no, not three taps deep in a detail page.
Three teams, one card
That card is where the division of labor got interesting.
The AI engineer owned what the model said: the prompts, the synthesis step, how careful a sketch should sound. The backend team owned everything around it — the endpoints, the storage, the orchestration that turned an upload into a profile. I owned the front end: the capture flow, the reveal, the swipe card, and every state the screen could be in while the model was still working.
Those three jobs move at different speeds. Prompts want to change all the time, because that’s how they get better. A backend wants its data model to change rarely. A front end wants something predictable to render. Put the three on one feature without an agreement and the default contract becomes “whatever the model returns this week.” Every prompt improvement turns into a possible UI bug, and the front end slowly becomes a parser for prose it doesn’t control.
We agreed early that we wouldn’t work that way.
What the contract actually was
The contract had two parts, and neither was sophisticated.
The first was a small API surface. The backend exposed fewer than ten endpoints, and all three teams agreed on them early:
- GET
/api/pets - List adoptable pets, with filters
- GET
/api/pets/{id} - Pet detail, including the AI-generated bio
- POST
/api/pets/{id}/analyze - Photo + video → personality
- POST
/api/swipe - Record a like or a pass
- GET
/api/matches - Mutual likes for the current user
- POST
/api/dog-matcher - Lost-dog matching
- GET
/health - Liveness probe for PM2
Most of the real engineering lived behind those routes. The surface stayed small on purpose, so each team could keep iterating on its side without asking the other two for permission.
The second part was the shape of the profile itself: a structured output that all three teams agreed on, sitting upstream of any LLM call. The idea was simple: the model fills in a shape, but it doesn’t get to invent one. Tags and scores come from the pipeline. Prose lives in exactly one field, the description, and even that one is editable by the shelter before anything is published.
// Simplified for this note. Tags and scores come from the
// pipeline; prose lives in exactly one field.
type PetProfile = {
breed: string; // from the photo, e.g. "Shiba mix"
ageBand: string; // from the photo, e.g. "Young adult"
traits: string[]; // soft tags for the swipe card
behavior: {
energy: number; // 0–100
emotionalStability: number;
playPreference: string; // e.g. "Chase play"
} | null; // null: no clip, photo-only profile
description: string; // Gemini's sketch; shelters can edit it
};
With that in place, the AI engineer could rewrite a prompt from scratch and the front end wouldn’t notice, as long as the output still fit the shape. That was the whole point.
AI
Fills it.
PromptsGemini synthesisTone of the sketch
Backend
Serves it.
PipelineStorageRoutes
Front end
Renders it.
Capture flowSwipe cardWaiting states
The contract
Changes only when all three agree
Profile shape · Fewer than ten routes · Honest empty states
Why it held
Looking back, five habits did most of the work. None of them are really about AI, which might be the actual lesson.
- Put the shape upstream of the model. The personality summary only worked because the structured output came first and the LLM wrote inside it. Once the shape existed, questions about wording became something the AI side could solve on its own, instead of a conversation that needed all three teams.
- Keep the surface small and boring. Fewer than ten routes is not an impressive number, and that’s the point. Every endpoint is a promise to two other teams. The fewer promises you make, the easier they are to keep.
- Give every field an honest empty state. The video was optional, so a profile could legitimately be photo-only. Generation takes time, so the page never blocked on the model; partial results streamed in as they arrived. That meant “not yet” and “not available” had to be normal states in the contract, not errors the front end discovered at runtime. A lot of cross-team friction is really two teams disagreeing about what missing data means.
- Keep the parts that need explaining out of the model. The match score isn’t generated by an LLM. It’s a weighted average of three factors — static profile fit, personality alignment, and activity compatibility — so when a match looks wrong, you can see which factor dragged it down. The lost-dog matcher follows the same logic: candidates are filtered to a 5 km radius and a 72-hour window before Gemini is called at all, and anything under 0.70 similarity shows up as a suggestion, never as an automatic match. Rules that live in code are rules every team can read.
- Make honesty part of the design, not just the copy. We framed the model’s output as a personality sketch, not a diagnosis, and shelter staff could edit it before publishing. That one decision shaped a dozen smaller ones: the editable description field, the soft personality tags, the explicit “AI-generated” label. It felt much closer to how AI should ship in consumer products than most launch demos do.
A lot of cross-team friction is really two teams disagreeing about what missing data means.
What “held” actually means
My case study says the contract held for two iteration cycles without renegotiation. That sounds modest, so it’s worth being specific about what it meant: across two cycles, the AI side kept iterating on prompts and the backend kept evolving behind its routes, but nobody had to reopen the shape of the card or the list of routes.
A contract that holds is mostly invisible. It shows up as meetings nobody had to schedule, and as changes that landed on one side of the line without anyone on the other side noticing. On team projects, that’s the real outcome — more than any single screen.
What I’d bring to the next team
The instinct I left Goodle with is to treat API contract design as a first-class deliverable, not something that falls out of the implementation.
If I were starting a shared AI feature tomorrow, I’d do a few things in the first week:
- Write down the shape the model has to fill before anyone argues about what it should say.
- Decide which changes should be invisible to everyone else, and draw the boundary around them.
- Agree on what “missing” means for every field while it’s still cheap to agree.
- Keep anything that needs explaining — scores, thresholds, rules — in code that all three teams can read.
Models get better every few months. The seams between teams don’t get better on their own; someone has to design them. On Goodle, part of that job was mine, and it’s the part of the project I learned the most from.
