14 KiB
Darano Monorepo — DDD / Clean Architecture Refactoring Plan
Executive Summary
The five codebases (api/, auth/, wallet/, AdminPanel/, and proto/) share the same general idea of layering but diverge in naming, directory structure, config libraries, where business logic lives, and — critically for AdminPanel — how they access data. AdminPanel bypasses all Go backend services and writes directly to the core database, making it a parallel implementation of business logic. This plan defines a unified target architecture and a phased migration that preserves functionality at every step.
1. Current State Analysis
1.1 Naming & Structure Inconsistencies
| Concern | api/ |
auth/ |
wallet/ |
|---|---|---|---|
| Business logic layer | handler/ (HTTP-only) |
usecase/ |
core/walletImp/ |
| Business logic layer (2nd) | service/ (gRPC client) |
(same as above) | core/marketImp/, core/alertImp/ |
| Data access | No repo (gateway only) | repository/ |
repository/ |
| Domain models | domain/stub/ only |
domain/db/ + domain/dto/ |
domain/db/ |
| Config library | knadh/koanf |
kkyr/fig |
kkyr/fig |
| Config global | config.Cfg |
config.Cfg |
config.Cfg |
| gRPC server impl | N/A (HTTP-only) | usecase/* implements gRPC server |
core/*Imp/* implements gRPC server |
Key observation: auth/ has the cleanest pattern (usecase/ for business logic), wallet/ is the most messy (core/ mixes gRPC server with business logic), and api/ is fine as an HTTP gateway.
1.2 AdminPanel Architecture — Direct DB Access, No Abstraction
AdminPanel has a fundamentally different problem: it bypasses all Go backend services and connects directly to the core database.
AdminPanel ──direct Postgres──→ core_db (same DB as Go services)
──→ default_db (Django's own tables)
──→ lite_db (SQLite, fallback)
Key problems:
-
managed = FalseGORM models copied manually: Every model insrc/coreLogic/models.pyhasmanaged = Falseanddb_table = "..."— they are manual replicas of the Go services' GORM models. Generated viainspectdb, never kept in sync.make protogenerates betterproto stubs, but Django models are not auto-generated from those. -
No service layer: The entire AdminPanel is a thin layer of Django admin widgets on top of raw SQL tables. Business logic is scattered across:
src/coreLogic/admin/asset.py— inline validation (check_token_policy,auto_gen) duplicating Go-side rulessrc/coreLogic/acl.py— permission rulessrc/usermapper/user_perm.py— asset access control- Inline admin methods (
save_model) that bypass Go services' validation entirely
-
No gRPC integration: The AdminPanel reads/writes directly to
core_db. No gRPC call to Go services. Means:- Admin edits to
Assetsbypass trustline updates - Admin edits to
Transactionsdon't trigger blockchain operations - Admin edits to
Walletsdon't update Stellar balances - Race conditions between admin edits and Go service operations
- Admin edits to
-
Hardcoded business rules in Django:
check_token_policyduplicates validation that should live in the wallet service.auto_gengenerates metadata that should be produced by the wallet service. -
Multi-database with fragile router:
src/adminpanel/db/routers.pyroutescoreLogicmodels tocore_db. If a new model is added to Go services but forgotten in Django admin, it silently fails. -
Inline business logic in Django admin:
MultiDBModelAdmin.save_modelhas asset-level access control.delete_modeldoes soft deletes. Mixed with admin layer, not separated.
1.3 Config Library Mismatch
api/usesknadh/koanf/v2with TOML parser. Tags arekoanf:"field-name".auth/andwallet/usekkyr/fig. Tags arefig:"field-name".- All three use a global singleton
config.Cfg. api/hardcodes defaults inParseConfig;auth/andwallet/rely on struct defaults.- AdminPanel uses
django-environ(.envfiles). Different language/framework entirely.
1.4 Where Business Logic Lives
- auth/
usecase/— Decent separation, but gRPC interfaces embedded directly in the struct. Validation, business rules, and gRPC response building mixed in the same methods. - wallet/
core/walletImp/— Problematic: 19 files implementingWalletServiceServerwith business logic, validation, DB calls, and Stellar blockchain calls all in one method. Example:UserInitWalletdoes validation, DB transactions, key generation, trustline creation, and status response — all in one method. - api/
handler/— Acceptable: handlers callDaranoService(gRPC client). No business logic here. Butservice/is a gRPC client aggregator with reflection-based URL lookup, not a traditional service layer. - AdminPanel
admin/— Wrong layer: business validation, metadata generation, and policy checks live in Django admin classes, not in a service layer.
1.5 Domain Model vs Persistence Model Confusion
All Go services use GORM models (domain/db/) directly as domain objects. No separation between domain entities (rich business objects with behavior) and persistence models (plain structs with GORM tags).
AdminPanel uses Django models with managed = False — the same conflation, but in Python.
2. Target Architecture
2.1 Go Services — Unified Directory Structure
Each Go service should follow:
service/
├── cmd/ # CLI entry points (Cobra)
├── domain/ # Innermost layer — pure business logic
│ ├── entity/ # Domain entities (rich, no persistence tags)
│ ├── valueobject/ # Immutable value objects (Money, AssetID, etc.)
│ ├── service/ # Domain services (orchestrate entities)
│ ├── repository/ # Repository INTERFACES only (ports)
│ ├── error/ # Domain-specific errors
│ ├── event/ # Domain events
│ ├── dto/ # Data transfer objects (gRPC/HTTP)
│ └── stub/ # Generated protobuf code (never edit)
├── application/ # Application layer — use cases
│ ├── usecase/ # Business logic / use cases
│ └── service/ # Application services
├── infrastructure/ # Infrastructure layer
│ ├── repository/ # Repository implementations (DB, Redis, Queue)
│ │ ├── db/ # PostgreSQL, etc.
│ │ ├── redis/ # Redis
│ │ └── queue/ # RabbitMQ
│ ├── config/ # Configuration loading
│ ├── grpc/ # gRPC server setup & registration
│ ├── logger/ # Logger initialization
│ └── crypto/ # Cryptography utilities
├── interface/ # Interface/Adapter layer
│ ├── http/ # HTTP handlers (api gateway only)
│ ├── grpc/ # gRPC server implementations
│ └── ws/ # WebSocket handlers
├── util/ # Cross-cutting utilities
├── go.mod
└── main.go
Dependency flow (points inward):
interface/ → application/ → domain/ ← infrastructure/
2.2 AdminPanel — Thin Admin Wrapper Around Go Services
AdminPanel needs a Django-native approach (no forced DDD naming), but the architectural principles are the same:
AdminPanel/src/
├── adminpanel/ # Django project config
├── coreLogic/
│ ├── domain/ # NEW: extracted business logic
│ │ ├── services/ # Validation, metadata generation (moved from admin/)
│ │ └── repositories/# Interfaces to backend
│ ├── application/ # NEW: use cases that delegate to gRPC
│ │ ├── asset_uc.py
│ │ ├── wallet_uc.py
│ │ └── market_uc.py
│ ├── infrastructure/ # NEW: gRPC client to Go services
│ │ └── grpc_client.py
│ ├── admin/ # Keep — now thin UI adapters only
│ ├── models.py # Keep as legacy (read-only ORM, managed=False)
│ └── enums.py # Keep
├── usermapper/ # Admin user management (keep as-is)
├── alerts/ # Notification system (keep as-is)
├── accounts/ # SMS accounts (keep as-is)
└── utils/ # Cross-cutting utilities
Key principle: AdminPanel should be a thin admin wrapper around the Go services, not a parallel implementation. Go services own all business logic. AdminPanel provides a human-friendly interface to view and (with validation) modify state.
2.3 AdminPanel Data Access Pattern
Read path: AdminPanel ──SQL──→ core_db (fast, direct, for list/detail views)
Write path: AdminPanel ──gRPC──→ Go services (validate + execute business logic)
2.4 Config Standardization
All Go services → knadh/koanf/v2 with TOML. No global singletons — config passed via constructor injection.
3. Migration Plan — Phased Approach
Phase 0: Preparation (1 day)
No code changes. Document test coverage, verify all services build, create tracking doc.
Phase 1: Standardize Go Config (2-3 days)
Replace fig → koanf in auth/ and wallet/. Move config to infrastructure/config/config.go. Remove global config.Cfg.
Risk: Low. Config struct fields stay the same; only the loader changes.
Phase 2: Reorganize auth/ (3-4 days)
Rename layers, move interfaces to domain/, move GORM models to infrastructure/. auth/ already has the cleanest pattern — this validates the approach.
Risk: Low. Structure change, minimal logic changes.
Phase 3: Reorganize wallet/ (1-2 weeks)
Rename core/ → application/. Split walletImp/, marketImp/, alertImp/ into application/usecase/. Move GORM models and interfaces. Split gRPC server registration from use cases. Move port/stellar/ → infrastructure/.
Risk: Medium. Incremental: rename first, then split gRPC, then add entities.
Phase 4: Restructure api/ (3-4 days)
Rename handler/ → interface/http/, service/ → infrastructure/grpcclient/, move middlewares/ → interface/http/middleware/.
Risk: Medium. Gateway pattern is different from auth/wallet.
Phase 5: AdminPanel Service Layer (2-3 weeks)
- Create
infrastructure/grpc_client.py— reusable gRPC client to Go services - Extract business logic from admin classes to
coreLogic/domain/services/ - Create use case layer
coreLogic/application/ - Refactor admin classes to thin adapters (delegate to use cases)
- Fix router typo (
no_migartion→no_migration) - Add gRPC write hooks for
save_model
Risk: Medium. Test every admin page after migration.
Phase 6: Shared Domain Types (3-5 days)
Extract WalletID, UserID, AssetID, Balance value objects into monorepo root shared/domain/. Import via each service's go.mod.
Risk: Medium. Affects all services on change.
Phase 7: Domain Events (2-3 days, optional)
Add domain/event/ to Go services for cross-domain communication.
4. Migration Order
Phase 0: Preparation
↓
Phase 1: Go config standardization
↓
Phase 2: Reorganize auth/ ──┐
├──→ Phase 5: AdminPanel (parallel with 2-4)
Phase 3: Reorganize wallet/ ─┘
↓
Phase 4: Restructure api/
↓
Phase 6: Shared domain types
↓
Phase 7: Domain events (optional)
5. Before/After: AdminPanel Asset Management
Before — business logic in Django admin:
# src/coreLogic/admin/asset.py
@admin.register(Assets, site=admin_site)
class AssetAdmin(MultiDBModelAdmin):
def save_model(self, request, obj, form, change):
if user_perm.can_access_asset(request.user, obj):
return super().save_model(request, obj, form, change)
# Direct DB write — bypasses wallet service!
def check_token_policy(self, request, obj: Assets) -> bool:
# Validation duplicated from wallet service
if obj.can_buy:
p = AssetPrices.objects.filter(asset=obj).last()
if not p or p.ico_price <= 0:
errors.append("ico price must be > 0")
After — thin admin delegating to Go service:
# src/coreLogic/application/asset_uc.py
class AssetUseCase:
def __init__(self, grpc_client: GRPCClient):
self.grpc_client = grpc_client
def update_asset(self, admin_user, asset_id, changes: dict) -> StatusRes:
"""Delegates all mutations to wallet service."""
return self.grpc_client.wallet_service.InternalWalletUpdateAsset(
AssetUpdateReq(id=asset_id, changes=changes)
)
# src/coreLogic/admin/asset.py
@admin.register(Assets, site=admin_site)
class AssetAdmin(MultiDBModelAdmin):
list_display = [...]
# ... UI config only
def save_model(self, request, obj, form, change):
uc = AssetUseCase(self.grpc_client)
uc.update_asset(request.user, obj.id, form.cleaned_data)
6. Risk Mitigation
| Risk | Mitigation |
|---|---|
| Breaking CI/CD | Keep make build and make run working at every phase |
| Lost functionality during rename | Use git mv + sed for mass renames; commit each file move separately |
| gRPC contract breaks | Proto definitions are in proto/. We only change Go implementation |
| Config migration issues | Keep old config format identical; only change the loader |
| AdminPanel write conflicts | gRPC write path ensures Go services own all mutations |
| Django model drift | Document which models are read-only ORM vs which need gRPC sync |
| Multiple air instances conflict | The flock mechanism in wallet Makefile handles this |
7. Non-Goals
- Changing protobuf definitions —
.protofiles stay as-is - Adding new frameworks — no ORM replacement
- Rewriting the UI
- Complete test suite rewrite