Skip to content

Commit 91f707c

Browse files
authored
Merge pull request #6 from IDTS-LAB/normalized-user-domain-architecture-0d379
Update from task d9094924-b67f-44f0-bb30-c9ebeaa0d379
2 parents 4ec78ec + 5fefb72 commit 91f707c

22 files changed

Lines changed: 1162 additions & 105 deletions

.gitignore

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,48 @@ __pycache__/
33
*.pyc
44
*.pyo
55
*.pyd
6-
.pytest_cache/
7-
.coverage
8-
coverage/
9-
htmlcov/
106
*.log
117
*.tmp
128
*.swp
13-
*.swo
149
.DS_Store
1510
Thumbs.db
1611
.env
1712
.env.local
18-
*.env.*
13+
.env.*
1914
.vscode/
2015
.idea/
21-
.mypy_cache/
2216
node_modules/
2317
venv/
2418
.venv/
2519
dist/
2620
build/
2721
target/
2822
.gradle/
23+
.mypy_cache/
24+
.pytest_cache/
25+
coverage/
26+
htmlcov/
27+
.coverage
28+
*.zip
29+
*.gz
30+
*.tar
31+
*.tgz
32+
*.bz2
33+
*.xz
34+
*.7z
35+
*.rar
36+
*.zst
37+
*.lz4
38+
*.lzh
39+
*.cab
40+
*.arj
41+
*.rpm
42+
*.deb
43+
*.Z
44+
*.lz
45+
*.lzo
46+
*.tar.gz
47+
*.tar.bz2
48+
*.tar.xz
49+
*.tar.zst
2950
```

docs/NORMALIZED_USER_DOMAIN.md

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
"""Normalized User Domain Schema Design
2+
3+
This module implements a fully normalized user domain following:
4+
- PostgreSQL 17 features
5+
- Third Normal Form (3NF)
6+
- Domain-Driven Design (DDD) principles
7+
- Modular Monolith Architecture
8+
- CQRS compatibility
9+
- Multi-tenancy readiness
10+
- Audit-friendly design
11+
12+
## Domain Analysis: Why Monolithic Users Table is Bad
13+
14+
1. **Single Responsibility Principle Violation**: A monolithic users table mixes identity,
15+
profile, security, preferences, and contact information in one place. This makes it
16+
difficult to reason about and maintain.
17+
18+
2. **Scalability Bottlenecks**: As the table grows wide with many columns, every query
19+
loads unnecessary data. Index efficiency decreases, and vacuum operations become slower.
20+
21+
3. **Security Concerns**: Sensitive data like password hashes and security settings should
22+
be isolated from frequently accessed profile data to minimize exposure surface.
23+
24+
4. **Multi-Tenancy Complexity**: Adding tenant isolation to a wide table requires careful
25+
consideration of which fields need tenant scoping.
26+
27+
5. **CQRS Incompatibility**: Command and Query Responsibility Segregation becomes difficult
28+
when read models need different projections than write models from the same table.
29+
30+
6. **Microservice Extraction**: When extracting services, a monolithic table creates tight
31+
coupling. Separated tables allow clean bounded context boundaries.
32+
33+
7. **Audit Trail Gaps**: Tracking changes across many unrelated fields in one table is
34+
complex and error-prone.
35+
36+
8. **Performance Contention**: Hot spots form when unrelated operations compete for locks
37+
on the same row.
38+
39+
## Recommended Normalized Structure
40+
41+
### Identity Bounded Context
42+
- **users**: Core identity and authentication credentials
43+
- **user_security**: Security state, lockouts, MFA configuration
44+
- **user_verifications**: Verification status per communication channel
45+
- **user_sessions**: Active sessions and refresh tokens
46+
47+
### Profile Bounded Context
48+
- **user_profiles**: Personal information (names, bio, avatar)
49+
- **user_contacts**: Multiple contact methods with types
50+
- **user_addresses**: Multiple addresses with labels
51+
- **user_settings**: User preferences in flexible JSONB format
52+
53+
### Access Control Bounded Context
54+
- **roles**: Role definitions
55+
- **permissions**: Permission definitions
56+
- **role_permissions**: Role-to-permission assignments
57+
- **user_has_roles**: User-to-role assignments
58+
59+
### Audit Bounded Context
60+
- **audit_logs**: Immutable event log for compliance
61+
- **error_traces**: Error tracking for debugging
62+
63+
## ERD Diagram (Mermaid)
64+
65+
```mermaid
66+
erDiagram
67+
users ||--o| user_profiles : "has"
68+
users ||--o| user_security : "has"
69+
users ||--o| user_contacts : "has multiple"
70+
users ||--o| user_addresses : "has multiple"
71+
users ||--o| user_settings : "has"
72+
users ||--o| user_verifications : "has multiple"
73+
users ||--o| user_sessions : "has multiple"
74+
users ||--o| user_has_roles : "assigned"
75+
76+
roles ||--o{ role_permissions : "contains"
77+
permissions ||--o{ role_permissions : "contained in"
78+
roles ||--o{ user_has_roles : "assigned to"
79+
users ||--o{ user_has_roles : "has roles"
80+
81+
authorization_resources ||--o{ permissions : "defines"
82+
83+
users {
84+
uuid id PK
85+
varchar email UK
86+
varchar username UK
87+
varchar password_hash
88+
varchar auth_provider
89+
varchar status
90+
timestamptz created_at
91+
timestamptz updated_at
92+
}
93+
94+
user_profiles {
95+
uuid id PK
96+
uuid user_id FK UK
97+
varchar first_name
98+
varchar last_name
99+
varchar display_name
100+
varchar avatar_url
101+
text bio
102+
date birth_date
103+
}
104+
105+
user_security {
106+
uuid id PK
107+
uuid user_id FK UK
108+
int failed_login_attempts
109+
timestamptz locked_until
110+
timestamptz password_changed_at
111+
boolean two_factor_enabled
112+
varchar two_factor_secret
113+
}
114+
115+
user_contacts {
116+
uuid id PK
117+
uuid user_id FK
118+
varchar type
119+
varchar value
120+
boolean is_primary
121+
boolean is_verified
122+
}
123+
124+
user_addresses {
125+
uuid id PK
126+
uuid user_id FK
127+
varchar label
128+
varchar line1
129+
varchar line2
130+
varchar city
131+
varchar state
132+
varchar postal_code
133+
varchar country
134+
boolean is_default
135+
}
136+
137+
user_settings {
138+
uuid id PK
139+
uuid user_id FK UK
140+
jsonb preferences
141+
}
142+
143+
user_verifications {
144+
uuid id PK
145+
uuid user_id FK
146+
varchar channel
147+
boolean is_verified
148+
timestamptz verified_at
149+
varchar verification_token
150+
}
151+
152+
user_sessions {
153+
uuid id PK
154+
uuid user_id FK
155+
varchar refresh_token_hash
156+
timestamptz expires_at
157+
varchar device_info
158+
varchar ip_address
159+
boolean is_revoked
160+
}
161+
162+
roles {
163+
uuid id PK
164+
varchar name UK
165+
varchar description
166+
}
167+
168+
permissions {
169+
uuid id PK
170+
uuid resource_id FK
171+
varchar key UK
172+
varchar resource
173+
varchar action
174+
varchar description
175+
}
176+
177+
authorization_resources {
178+
uuid id PK
179+
varchar key UK
180+
varchar name
181+
varchar description
182+
}
183+
184+
role_permissions {
185+
uuid id PK
186+
uuid role_id FK
187+
uuid permission_id FK
188+
}
189+
190+
user_has_roles {
191+
uuid id PK
192+
uuid user_id FK
193+
uuid role_id FK
194+
}
195+
196+
audit_logs {
197+
uuid id PK
198+
varchar action
199+
uuid actor_id
200+
varchar resource_type
201+
uuid resource_id
202+
varchar request_id
203+
jsonb meta
204+
timestamptz created_at
205+
}
206+
```
207+
208+
## DDD Mapping
209+
210+
### Aggregates
211+
- **UserAggregate**: Root entity `users` with entities `user_security`, `user_profiles`
212+
- **SessionAggregate**: Root entity `user_sessions`
213+
- **RoleAggregate**: Root entity `roles` with `role_permissions`
214+
215+
### Entities
216+
- `users`: Identity aggregate root
217+
- `user_security`: Security configuration entity
218+
- `user_profiles`: Profile entity
219+
- `user_contacts`: Contact method entity
220+
- `user_addresses`: Address entity
221+
- `user_sessions`: Session entity
222+
- `roles`: Role aggregate root
223+
- `permissions`: Permission entity
224+
225+
### Value Objects
226+
- `Email`: Email address with validation
227+
- `PhoneNumber`: Phone number with formatting
228+
- `Address`: Structured address components
229+
- `Preferences`: JSONB settings object
230+
231+
### Domain Services
232+
- `AuthenticationService`: Login/logout/password management
233+
- `AuthorizationService`: RBAC evaluation
234+
- `VerificationService`: Email/phone verification
235+
- `SessionService`: Session lifecycle management
236+
- `AuditService`: Audit log creation
237+
238+
## Future Scalability Considerations
239+
240+
### Millions of Users
241+
- Partition `audit_logs` and `user_sessions` by date
242+
- Use connection pooling efficiently
243+
- Implement read replicas for query separation
244+
- Cache frequently accessed profiles
245+
246+
### Multi-Tenancy
247+
- Add `tenant_id` column to all tables
248+
- Implement Row Level Security (RLS) policies
249+
- Use schema-per-tenant for high isolation needs
250+
251+
### OAuth/SSO Support
252+
- `auth_provider` field supports external identity providers
253+
- `external_id` can be added for provider-specific IDs
254+
- `user_verifications` tracks OAuth account linking
255+
256+
### Microservice Extraction
257+
- Each bounded context can become a separate service
258+
- Clear foreign key boundaries enable database splitting
259+
- Event sourcing ready via `audit_logs`
260+
261+
### Event-Driven Architecture
262+
- `audit_logs` serves as event store
263+
- Can integrate with message brokers (Kafka, RabbitMQ)
264+
- Supports CQRS read model rebuilding
265+
266+
"""
Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,39 @@
11
from uuid import UUID
22

3-
from sqlalchemy import String, UniqueConstraint
4-
from sqlalchemy.orm import Mapped, mapped_column
3+
from sqlalchemy import String, UniqueConstraint, Index
4+
from sqlalchemy.orm import Mapped, mapped_column, relationship
55

66
from src.shared.database.mixin.timestamp import SoftDeleteMixin, TimeStampMixin
77
from src.shared.database.model import Base
88

99

1010
class PermissionModel(Base, TimeStampMixin, SoftDeleteMixin):
11+
"""Permission definition for RBAC system.
12+
13+
Permissions represent specific actions on resources.
14+
Linked to authorization_resources for resource management.
15+
"""
1116
__tablename__ = "permissions"
1217
__table_args__ = (
1318
UniqueConstraint("resource", "action", name="uq_permissions_resource_action"),
19+
Index("ix_permissions_key", "key", unique=True),
20+
Index("ix_permissions_resource", "resource"),
21+
Index("ix_permissions_action", "action"),
22+
Index("ix_permissions_resource_id", "resource_id"),
1423
)
1524

16-
key: Mapped[str] = mapped_column(String(255), unique=True, index=True)
17-
resource_id: Mapped[UUID] = mapped_column(index=True)
18-
resource: Mapped[str] = mapped_column(String(100), index=True)
19-
action: Mapped[str] = mapped_column(String(100), index=True)
25+
key: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
26+
resource_id: Mapped[UUID] = mapped_column(nullable=False)
27+
resource: Mapped[str] = mapped_column(String(100), nullable=False)
28+
action: Mapped[str] = mapped_column(String(100), nullable=False)
2029
description: Mapped[str | None] = mapped_column(String(255), nullable=True)
30+
31+
# Relationships
32+
roles: Mapped[list["RolePermissionModel"]] = relationship(
33+
back_populates="permission",
34+
cascade="all, delete-orphan",
35+
)
36+
authorization_resource: Mapped["AuthorizationResourceModel"] = relationship(
37+
back_populates="permissions",
38+
foreign_keys=[resource_id],
39+
)

0 commit comments

Comments
 (0)