Repository navigation
feat(xtm-oaev-sdk): PoC architecture comparison, flat vs ddd #301
Description
TL;DR
- Two branches, two internal architectures, same public API, same 513 tests, both green.
- exploration/sdks-flat (~15 files) vs exploration/sdks-ddd` (~30 files), pick one for the squad.
- Extracts the shared data-layer out of
pyoaev(62-file monolith) so it becomes a pure framework client. - Decision matrix ready; four open questions need team answers before merging anything.
- PoC only, no merge target, created after exploration. Origin: {Analysis} Exploration in the purpose of potential split. #199.
Context
This issue is the anchor for the three-SDK architecture comparison. The satellites (collectors-sdk and injectors-sdk) reference this issue for shared context and decision framing.
Work was done exploration-first: both architectures are fully implemented and tested before these issues were created.
The goal is to validate a structural decision as a team at the squad meeting, not to merge a PR today.
Origin analysis: #199.
Why this SDK exists
pyoaev is currently a 62-file monolithic package that mixes:
- API client framework
- Shared data types
- Security domain logic
- Signature verification
- Daemon lifecycle management
- Configuration
xtm-oaev-sdk extracts the shared data-layer: types, config, contracts, signatures, exceptions, helpers.
So that pyoaev becomes a pure framework client.
Extension SDKs (collectors-sdk, injectors-sdk) and future squads depend on this extracted layer, not on the monolith.
SDK hierarchy:
xtm-core-sdk (planned)
└── xtm-oaev-sdk ← this issue
├── pyoaev (post-extraction)
├── collectors-sdk
└── injectors-sdk
Dependency direction is strictly one-way.
Extension SDKs never depend on each other.
Scope promotion rules:
if both extension SDKs need a symbol → promote to xtm-oaev-sdk;
if both squads need it → promote to xtm-core-sdk.
Two branches, two architectures
| Branch | Location |
|---|---|
exploration/sdks-flat |
https://github.com/OpenAEV-Platform/client-python/tree/exploration/sdks-flat |
exploration/sdks-ddd |
https://github.com/OpenAEV-Platform/client-python/tree/exploration/sdks-ddd |
Both branches expose identical public symbols. Both produce the same __all__. Same test suite, same assertions. Both are green.
Architecture A: Flat-First (exploration/sdks-flat)
Structure: 2 layers, ~15 files.
xtm-oaev-sdk/
├── _core/ # private implementation
└── __init__.py # re-exports only
- All symbols live in
_core/as flat modules. - No feature folders, everything at the same depth.
- Fast to navigate, minimal ceremony, obvious entry point.
- Lower file count = lower cognitive overhead for a new contributor.
Trade-off: As the symbol count grows, _core/ becomes a flat dumping ground.
No enforced boundary between features.
A breaking change in one area is indistinguishable from a change in another.
Architecture B: DDD + Light Hex (exploration/sdks-ddd)
Structure: 3 layers, ~30 files.
xtm-second-oaev-sdk/
├── _core/ # private domain, feature folders
│ ├── configuration/
│ ├── contracts/
│ ├── errors/
│ ├── signatures/
│ └── ...
├── contracts/ # stable boundary, re-exports only, one file per feature
└── public/ # flat user-facing, re-exports only
Three named layers:
_core/: private domain logic, never imported directly by consumers.contracts/: stable boundary; re-exports only; this is what other SDKs depend on. A change here is a deliberate, visible contract change.public/: flat, user-facing; re-exports fromcontracts/.
Feature folder convention: features are first-class structural units.
Each feature gets its own folder in all three layers.
Sub-components live inside their parent feature folder unless genuinely standalone (no micro-feature split).
Template-aligned role decomposition inside features:
_core/<feature>/
├── engines/
├── internals/
├── models/
├── protocols/
├── types/
└── utils/
Trade-off: ~2× the file count. contracts/ layer is ceremony, it adds a stable boundary but requires every contributor to understand the three-layer contract.
Onboarding takes longer.
Decision matrix
Both architectures are evaluated on 8 criteria. Neither is universally superior.
| Criterion | Weight | Flat-First | DDD + Light Hex |
|---|---|---|---|
| Simplicity | High | ✓ | |
| Feature isolation | Medium | ✓ | |
| Breaking change control | High | ✓ | |
| Onboarding speed | Medium | ✓ | |
| Scalability | Medium | ✓ | |
| Test boundaries | Low | ✓ | |
| File count / navigation | Low | ✓ | |
| Self-documenting structure | Medium | ✓ |
File count comparison across all three SDKs:
| SDK | Flat-First | DDD + Light Hex |
|---|---|---|
| xtm-oaev-sdk | ~15 files | ~30 files |
| collectors-sdk | ~10 files | ~20 files |
| injectors-sdk | ~8 files (~17 .py) | ~16 files (~29 .py) |
Hybrid option: Start flat, promote to DDD on a concrete trigger. This is viable but the trigger must be named explicitly, "when it gets complex" is not a trigger.
Migration strategy (architecture-agnostic)
Three-phase deprecation shims in pyoaev for all moved symbols:
| Phase | Warning type | Visibility |
|---|---|---|
| Phase 1 (now) | DeprecationWarning |
Test output only |
| Phase 2 | FutureWarning |
Production visible |
| Phase 3 | Remove shim | Old import path stops working |
xtm-oaev-sdk deprecation shims:
| Area | Shim count |
|---|---|
| signatures | 22 |
| exceptions | 13 |
| configuration | 6 |
| utils | 6 |
| SSL | 4 |
| daemon protocol | 1 |
Four open questions for the squad meeting
These must be answered before a structural decision can be made:
- Growth rate: How many features per SDK in the next year? (current counts: injectors=2, collectors=1, xtm-oaev-sdk=7 groups)
- Ceremony tolerance: Is the team OK enforcing the
contracts/layer as a strict boundary for all contributors? __init__.pydiscipline: Do we commit to__init__.py= re-exports only, regardless of architecture?- Hybrid trigger: If we go hybrid (flat → DDD on trigger), what is the concrete, named condition for promotion?
Validation facts
- 513 tests pass on both architectures. Same suite, same assertions, both green.
- 180 total public symbols across all 3 SDKs (107 + 43 + 30).
- Both produce the same
__all__, consumers cannot tell the architectures apart from the outside.
This is a PoC
These branches exist for architecture validation at the squad meeting. There is no merge target. The goal is a team decision, not a shipping artifact.
Related satellite issues:
OpenAEV-Platform/collectors#450
OpenAEV-Platform/injectors#286
Activity