Why I Didn’t Store Contact Information Directly in User Accounts
A small architectural decision that makes identity systems easier to scale and maintain.
A typical schema looks like this:
User
├── email
├── phone_number
├── telegram
├── whatsapp
└── ...
It feels simple — until the User entity accumulates responsibilities that don’t belong to it.
The problem in one diagram
People rarely have a single forever-email or phone. Channels multiply; apps invent new ones.
Thinking in aggregates
Instead of stuffing channels into User, I introduced a Contact aggregate:
User Account
│
├── Email
├── Mobile
├── Telegram
└── WhatsApp
- User → identity within the system
- Contact → how you reach them
- Each aggregate owns its rules and lifecycle
What each contact can own
Verification, encryption, and validation differ by channel. That’s easier when contacts are first-class:
Benefits
1. Extensibility
New contact type → no User-model migration for every product change.
2. Separation of responsibilities
Accounts and channels change for different reasons. Independence reduces accidental complexity.
3. Better security controls
Different channels need different verification workflows, encryption policies, and validation rules.
4. More realistic modeling
People change numbers. They add Telegram. They retire an email. The model should allow that without pretending everyone has exactly one of each forever.
How accidental complexity grows
Day one
One email column. Ship it.
Month three
Secondary phone. “Just one more field.”
Year one
Exceptions, nulls, and half-verified channels living on User.
The refactor
Contact aggregate — what you wished you had on day one.
The point
The goal wasn’t more tables. The goal was clearer boundaries: access vs communication.
Continue with Citizenship as a Relationship.
Canonical version. Also on Medium.