🌐 Language: English Version | 中文版
As products evolve, login options multiply: username, email, phone number, plus Google, GitHub, WeChat, and enterprise SSO. Traditional “wide table” user designs face a dilemma: constantly adding fields leads to bloated schemas, or limiting login methods sacrifices user experience. This article explores a domain model that separates AuthIdentity from User to build an extensible, unified authentication system.
TL;DR
- Separate concerns: Split “who the user is” (User) from “how to prove it” (AuthIdentity)
- Zero schema changes: Adding new login methods only requires INSERT, not ALTER TABLE
- Flexible progression: Support “passwordless” users who add credentials over time
- Natural conflict prevention: Composite unique index
(identity_type, identifier)prevents duplicate bindings
1. From “Wide Table” to “Separation Model”
1.1 The Pain of Traditional Design
Early user tables typically looked like this:
1 | CREATE TABLE users ( |
The problems are obvious:
- Poor extensibility: Adding login methods requires schema changes—risky for large production tables
- Tight coupling: Password field is mandatory for all users, blocking “SMS-only” or “social-only” scenarios
- Chaotic uniqueness:
username,email,mobileeach have unique constraints, but social IDs scatter across different fields - Data redundancy: Passwordless users have empty
password_hash; users without email have emptyemail
1.2 Core Idea of the Separation Model
Completely decouple “who the user is” (User) from “how to prove you’re you” (AuthIdentity):
- User: Only cares about basic user info, not login methods
- AuthIdentity: Only cares about authentication credentials, not other user data
- UserPassword: Password as an independent entity, supporting “passwordless user” scenarios
The immediate benefit: Adding a new login method only requires inserting one AuthIdentity record—no schema changes needed.
2. Domain Model Design
2.1 Core Entity Relationships
classDiagram
class User {
+Long id
+String nickname
+String avatar
+String status
+datetime created_at
}
class AuthIdentity {
+Long id
+Long user_id
+String identity_type
+String identifier
+datetime verified_at
}
class UserPassword {
+Long user_id
+String password_hash
}
User "1" --> "n" AuthIdentity : has
User "1" --> "0..1" UserPassword : has
2.2 Entity Responsibility Boundaries
User: The User Aggregate Root
The User entity stores only business-agnostic basic information:
1 | public class User { |
Design Intent: The User table should be stable. Regardless of how many login methods are supported later, this table never needs changes. It answers “does this user exist?” not “how does this user log in?”
AuthIdentity: Authentication Identity
AuthIdentity records all login credentials a user possesses—one record per login method:
1 | public class AuthIdentity { |
Key Design Points:
- Composite unique index:
(identity_type, identifier)ensures one email can’t bind to two accounts - Flexible identifier:
13800138000can be a mobile-type identifier or a username-type identifier—no conflict - Minimal core fields: AuthIdentity stores only the minimum info needed to locate users; extended data can be stored separately
UserPassword: Independent Password Entity
1 | public class UserPassword { |
Why a separate table?
- Support passwordless users: Users with only SMS or social login have no UserPassword record
- Independent password policy: Can apply stronger security controls (stricter access control, audit logs)
- Password history: If you need “can’t reuse last 5 passwords” policy, can extend to one-to-many relationship
3. Model in Action
3.1 Scenario 1: User Registration
Email registration:
1 | -- 1. Create User |
WeChat one-click registration (passwordless user):
1 | -- 1. Create User |
3.2 Scenario 2: Adding Login Methods
User originally logs in with email, now wants to bind mobile:
1 | -- Just insert one record—no changes to users table |
User wants to also bind GitHub:
1 | INSERT INTO auth_identities (user_id, identity_type, identifier) |
Feel the extensibility: Each new login method is just one INSERT. In system code, it’s just adding one enum value for identity_type.
3.3 Scenario 3: Login Flow Collaboration
User enters john@example.com + password to log in:
1 | public LoginResult login(String input, String password) { |
Note the query path: AuthIdentity → User → UserPassword. Each entity has its own responsibility.
3.4 Scenario 4: Social Account Binding Conflict
User A is logged in, tries to bind a WeChat already bound to User B:
1 | public void bindSocialAccount(Long currentUserId, String wechatOpenid) { |
Key Point: The (identity_type, identifier) unique constraint naturally prevents duplicate bindings. Conflict handling just needs simple logic.
4. Architectural Advantages
4.1 Extensibility: Zero Schema Changes for New Login Methods
Compare with traditional design:
| Approach | Cost of Adding Login Method |
|---|---|
| Wide Table | ALTER TABLE to add column—high risk for large tables, requires gradual rollout |
| Separation Model | INSERT one record, add enum value in code—pure business logic change |
4.2 Flexibility: Support “Progressive” Authentication
Users can start with a “passwordless” account (social login only), then add password and bind mobile later:
1 | -- Initial state: only WeChat login |
4.3 Data Consistency: Unbinding Doesn’t Lose User Data
Unbinding mobile just deletes one AuthIdentity record:
1 | DELETE FROM auth_identities |
The User entity is unaffected—user’s orders, posts, comments remain. As long as at least one AuthIdentity remains, the user can still log in.
5. Implementation Notes
5.1 Index Design
1 | -- Core query: find user by login method |
5.2 Transaction Boundaries
Creating a user requires consistency across three tables:
1 |
|
5.3 Security Considerations
- Verification codes: Used to verify phone/email ownership; mark
verified_atafter verification - Password policy: Use Argon2id, recommended parameters
m=65536, t=3, p=4 - Token storage: Social account AccessTokens should be stored separately and encrypted
- Brute force protection: Rate limit by
identifier+identity_typedimension, not just IP
6. Comparison: When to Use This Model
| Scenario | Wide Table | Separation Model |
|---|---|---|
| Only username + password | ✅ Simple | Overkill |
| Username + email + mobile | ⚠️ Manageable | ✅ Better |
| Multiple social logins + SSO | ❌ Painful | ✅ Essential |
| Need passwordless option | ❌ Hard | ✅ Natural |
| Frequent login method changes | ❌ Schema migrations | ✅ Just INSERT |
7. Key Takeaways
The core value of separating AuthIdentity from User: decouple “identity” from “authentication method”.
This design enables systems to:
- Scale flexibly: Add login methods with just records, no schema changes
- Evolve progressively: Support users moving from “social login” to “multi-factor authentication”
- Protect data: Unbinding login methods doesn’t lose user business data
- Maintain clarity: Each entity has single responsibility, code is easier to maintain
When your product evolves from “username/password only” to “supporting phone, email, WeChat, GitHub, enterprise SSO”, this separation model saves you from painful schema migrations and makes authentication a truly evolvable business capability.