Skip to content

Commit 5fefb72

Browse files
committed
Title: Implement Normalized User Domain with DDD Structure and Update Handlers
Key features implemented: - New docs/NORMALIZED_USER_DOMAIN.md detailing normalized schema design rationale and structure - New src/modules/user/infrastructure/models/__init__.py aggregating all user domain models - New src/modules/user/infrastructure/models/user_*_model.py files for normalized user entities (address, contact, profile, security, settings, verification) - Updated src/modules/authorization/infrastructure/models/*_model.py with indexing optimizations - Updated src/modules/user/application/auth/login_user/handler.py to reference password_hash correctly - Updated src/modules/user/application/auth/register_user/handler.py to use password_hash in User.create - Updated src/modules/user/application/detail_user/handler.py to fetch user with relations via get_by_id_with_relations - Updated src/modules/user/domain/entities/user.py with normalized entity structure including profile, settings, security - Updated src/modules/user/domain/repositories/user_repository.py interface for relation handling - Updated src/modules/user/infrastructure/models/user_model.py to reflect normalized structure and relationships - Updated src/modules/user/infrastructure/repositories/user_repository.py implementation for normalized data access The changes implement a fully normalized user domain following DDD principles, separating concerns into distinct bounded contexts while updating application handlers to utilize the new structure. The repository layer now supports fetching related user data efficiently.
1 parent 4ec78ec commit 5fefb72

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)