Domain-First Development: Actually Building What We Designed
June 2, 2025
The decision was made: Time to start over. I’d synthesized the advice of multiple thinking parters and designed a $0 infrastructure. I’d written about the vision of a strategic thinking partner.
Now came the hard part: actually building it.
Starting where it matters
The temptation was strong to dive straight into the AI integration. After all, this was an AI product, right? But years of watching projects fail (and a bit of asking around for advice from several LLMs) taught me better. Start with the domain.
While Docker containers spun up in the background (PostgreSQL, Redis, ChromaDB, Temporal, Traefik — the whole orchestra), I opened a single file: models.py.
python@dataclass
class Product:
id: str
name: str
vision: str
strategy: str
metrics: List['Metric']
features: List['Feature']
stakeholders: List['Stakeholder']This wasn’t code. This was philosophy. Every line was a statement about what product management actually is.
I was reminded of realizing back in the day (maybe when I was still at Yahoo) that a great User Experience started with the data structures. By the time you’re painting the interface on, it’s too late!
This is likely doubly true now that the presentation layer is evolving into this fuzzy language-based form of interaction.
Successful prototype syndrome
Here’s something nobody tells you about successful prototypes: they’re dangerous. The POC worked. It created GitHub issues, reviewed tickets, ingested documents. It would have been so easy to just… keep building on it.
But that’s how you end up with a GitHub issue creator that happens to do PM tasks, instead of a PM assistant that happens to create GitHub issues.
The domain models forced the right perspective:
WorkItemnotGitHubIssueStakeholdernotGitHubUserFeaturenotPullRequest
GitHub would be a plugin. So would Jira. So would whatever the hot new tool would be next year.
The SSH authentication dance
Of course, no development session is complete without authentication issues. I kept following advice to use obsolete GitHub workflows and would only realize when it asked for my password and then rejected the login saying GitHub doesn’t support that form of authentication anymore.
Then I’d remember I had SSH keys set up and use that instead. But the dance continued.
bash
git push origin main
Permission denied (publickey)But I have SSH keys! They’re right there!
Turns out you can have SSH keys got GitHub that only let you read stuff and not write it. Why did I set one of thus up at first? I seriously can’t remember.
Also, having SSH configured for GitHub and having it configured for this repository are also different things.
Twenty minutes later I found (or maybe created again from scratch) the right kind of PAT (personal access token) and was able to move on, for now.
Dependencies: the hidden time sink
With models defined and Git finally cooperating, time to install dependencies. How hard could it be? (Readers of this series may already have an idea.)
pip install -r requirements.txtIt was, in fact, quite hard.
The AI ecosystem in 2025 is… energetic. Packages update daily. Breaking changes are a lifestyle choice. My terminal became a waterfall of version conflicts:
langchain 0.1.0 requires pydantic>=2.0
but anthropic 0.18.1 requires pydantic<2.0
but openai 1.0.0 requires pydantic>=2.0
but wait chromadb needs...Solution? Start minimal. Add only what you need, when you need it. The final requirements.txt was surprisingly short:
fastapi
anthropic
openai
sqlalchemy
redisEverything else could wait.
Infrastructure: when it just works
Here’s the beautiful thing about that bootstrap script: it actually worked. One command and I had enterprise-grade infrastructure running locally:
./bootstrap-piper-1.0.shFive minutes later, Temporal’s workflow engine was humming, PostgreSQL was ready for data, Redis was caching nothing (but ready to cache everything), and Traefik was routing traffic like a champ.
Boring infrastructure is good infrastructure.
The first real test
With domain models defined and infrastructure running, time for the moment of truth. Could this architecture actually process PM requests?
@app.post("/api/v1/intent")
async def process_intent(request: IntentRequest):
# For now, just echo back with mock classification
return {
"intent": "CREATE_WORK_ITEM",
"confidence": 0.95,
"suggested_action": "Create a new work item",
"domain": "execution"
}Not exactly AI-powered yet, but the shape was right. The domain concepts were driving the API, not the other way around.
Multi-laptop reality
Then came anotherreal test: could I work on this from both laptops?
The work laptop (kindbook) had everything set up. The personal laptop (faoilean)… did not.
# On personal laptop
git clone git@github.com:mediajunkie/piper-morgan-platform.git
Permission denied (publickey)Different laptop. Different SSH keys. Different dance.
But once the authentication tango was done, something magical happened. Pull the code, run the bootstrap script, and boom — identical development environment. The Docker approach was proving its worth.
The power of boring
By the end of the session, I had:
- Domain models that actually modeled the domain
- Infrastructure that just worked
- Version control that (eventually) cooperated
- A foundation ready for AI integration
No AI responses yet. No clever features. Just solid, boring, foundational work.
And that’s exactly what was needed. Boring gets the donuts made.
Lessons from my new Day One
Starting over taught me:
- Domain models are your North Star — they keep you oriented when the implementation gets messy
- SSH authentication is worth the pain — but document every step
- Minimal dependencies win — you can always add more later
- Boring infrastructure is beautiful — excitement should come from features, not ops
- Multi-machine development is a feature — design for it from day one
Next steps
With the foundation in place, it was time to make it smart. Real LLM integration was next. But that’s a story for another post.
For now, I had something more important than AI responses: I had an architecture that could grow without falling apart. The domain was driving the design. The infrastructure was invisible. The stage was set.
Sometimes the best AI work doesn’t involve any AI at all.
