A client's mobile app and web app showed different data for the same user. Not because of a bug — because two different developers built two different data access patterns to serve two different UIs, and over time they drifted apart. One read from the database directly. The other went through a service layer that had its own caching logic. Same data, different results.

This happens when you build screens first and figure out the data layer as you go. The alternative — starting with domain objects and APIs — prevents an entire category of architectural problems.

Domain Objects First

Before you sketch a single screen or write a line of code, identify your nouns. What are the core things in your system?

For a field service management app: Equipment, People, Projects, Maintenance Items, Inspections. For an e-commerce platform: Products, Orders, Customers, Inventory, Shipments. For a SaaS tool: Users, Organizations, Subscriptions, Documents, Integrations.

These are your domain objects. They're the foundation of your data model, your API, and your access control system. Get them right and the rest of the architecture follows naturally. Get them wrong and you'll be refactoring for years.

Operations Second

For each domain object, what can you do with it? Start with the basics — create, read, update, delete — then add domain-specific operations.

Equipment: create, read, update, delete, transfer (to a new site), decommission, schedule_maintenance. People: create, read, update, deactivate, assign_role, revoke_role. Maintenance Items: create, read, update, complete, escalate, reassign.

Each operation becomes an API endpoint. The important discipline is that all interactions with domain objects go through these endpoints — from the web app, the mobile app, AI agents, partner integrations, everything. No backdoors. No "the mobile app reads the database directly because the API was too slow."

If the API is too slow, fix the API. Don't work around it.

Access Control Third

Now map who gets access to which operations on which objects. This is where roles become essential.

Don't design for people — design for roles. Don't build a screen for "Miguel" — build capabilities for the Operator role, the Mechanic role, the Fleet Manager role. One person might hold multiple roles. The navigation and permissions need to accommodate role-switching, not just one hardcoded view per person.

The access control matrix is simple: Role × Object × Operation = Allow/Deny. An Operator can read Equipment and create Maintenance Items but can't decommission Equipment. A Fleet Manager can do everything an Operator can plus transfer and decommission Equipment. An Admin can manage People and Roles.

This matrix translates directly into authorization middleware in your API. Every endpoint checks: does the requesting user's role allow this operation on this object type? This is role-based access control (RBAC), and it's the right model for 90% of applications.

Why This Enables AI

Here's the forward-looking reason API-first architecture matters more than ever: AI agents consume APIs, not screens.

When you build an AI assistant that helps users interact with your product, the agent needs to perform operations on domain objects — exactly the same operations your human users perform. If your architecture is API-first, the AI agent talks to the same endpoints as the web and mobile apps. The access control is the same. The validation is the same. The audit trail is the same.

The pattern I recommend: put MCP (Model Context Protocol) servers in front of your domain APIs. Your AI agent talks to the MCP servers, which translate natural language operations into API calls. This creates a clean abstraction layer for AI-powered features while keeping your domain logic intact and your access control enforced.

Schema Enforcement

If your team can't maintain clean data structures with a schema-flexible database (Firestore, MongoDB), switch to SQL where the schema is the enforcement. I've seen this pattern repeatedly: a NoSQL database gives the team freedom, and the team uses that freedom to create inconsistent data structures that make querying unreliable and migrations terrifying.

There's nothing wrong with NoSQL for the right use cases. But if your data has relationships, your operations require transactions, and your team needs guardrails, a relational database with enforced schemas is the pragmatic choice.

If you want consistent data models across your system, have spec files that generate the code — not hand-coded models that drift. If the team doesn't understand that pattern yet, use the database schema as the spec. At least then the database enforces consistency even if the application code doesn't.

The Monorepo Advantage

When you have multiple clients (web, mobile, possibly desktop or third-party integrations) sharing the same API, a monorepo keeps everything in sync. When you fix a bug in the Equipment domain object, the fix should be one PR across web and mobile — not separate commits to separate repos with separate deployment timelines.

A monorepo forces you to think in terms of "the equipment fix" rather than "the web fix" and "the mobile fix." It also means shared types, shared validation logic, and shared test utilities. The API contract is defined once and consumed everywhere.


Related: Monolith vs. Microservices: The Pragmatic Answer | The Prototype-to-Production Gap | Vertical Slice Development