epic(legibility): big-bang restructure — backend layer→domain, frontend lib→domain #406
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Epic context
This is Epic 4 of 5 for the Codebase Legibility Refactor (see #387).
This epic contains the actual structural change. Two large mechanical PRs (one per stack) restructure the codebase from layer-based to domain-based packaging. Domain names are stack-symmetric per the canonical domain set in #387 first comment.
This is gated by Epic 1 (Audit, #387) and Epic 3 (Pre-flight, #402). Do not start REFACTOR-1 or REFACTOR-2 until:
Scope
controller/,service/,repository/,model/,dto/,exception/,security/,config/→ per-domain packages +shared/lib/components/,lib/utils/,lib/hooks/, etc. → per-domain folders +lib/shared/Acceptance criteria
lib/layout matches and mirrors backend domain names per REFACTOR-2./mvnw test)npm run testandnpx playwright test)Definition of Done
This epic closes when REFACTOR-1, -2, -3, -4 are all closed AND a follow-up audit re-run (CLEANUP-5 in Epic 5) confirms the rubric checks above PASS.
Dependency
🏗️ Markus Keller — Senior Application Architect
Observations
controller/,service/,repository/,model/,dto/), a feature change forgeschichtecurrently touches 5 packages. After the refactor a developer goes togeschichte/and finds everything. That's the right end state.audit/andrelationship/. Those prove the pattern works and should be the reference for REFACTOR-1.dashboard/package is also partially domain-organized (DashboardController, DashboardService, DTOs co-located). This is evidence the team has already been organically moving toward domain packaging — REFACTOR-1 formalizes what's already happening.shared/category in the scope is the critical design decision. Looking at the current codebase, candidates are:exception/,security/,config/, and cross-cutting DTOs.GlobalExceptionHandlerandSecurityConfigclearly belong inshared/.DomainExceptionandErrorCodebelong inshared/exception/.MassImportServiceinjectsDocumentRepositorydirectly — after the refactor it would bedocument.DocumentRepositoryaccessed frommassimport/ordocument/. This is acceptable ifMassImportServiceis placed in thedocument/domain.SegmentationTrainingExportServiceinjectsAnnotationRepository,DocumentRepository, andTranscriptionBlockRepository— this is a genuine cross-domain service; it belongs inocr/and must call the other domains via their service interfaces.SenderModelServiceinjectsTranscriptionBlockRepository— cross-domain; resolve by callingTranscriptionServiceinstead or movingSenderModelinto theocr/domain entirely.TranscriptionQueueServiceinjectingDocumentRepositoryis the most dangerous pattern for ArchUnit — after the refactor it will immediately fail thetranscription→document.DocumentRepositoryrule. Plan this resolution explicitly before writing the ArchUnit test.Recommendations
shared/boundary criteria before writing a line of code. Rule: a class goes inshared/if and only if it is imported by ≥ 3 domains with no owning domain.DomainException,ErrorCode,Permission,SecurityConfig,AsyncConfig,WebConfigqualify. Entities that appear in cross-domain relationships (e.g.Personreferenced fromDocument) do NOT go inshared/— they stay in their owning domain and the referencing domain gets a summary projection.Class → Current package → Target domain package. This is the migration map and prevents mid-PR confusion about whereOcrJobDocumentgoes.lib/components/flat directory has 81 of 102 components unorganized. Thedocument/andchronik/subdirectories already exist and are working correctly. Mirror those as the pattern. Note:importpaths change, so allroutes/files that import from$lib/components/SomeThingwill need updating — that's the actual work, not the moves themselves.Open Decisions
MassImportServicelive after the refactor? It touchesdocument/,person/,tag/, andfile/domains. Options: (a) put it indocument/since that's the primary output — and accept it callsPersonService,TagServicevia their service interfaces; (b) create animport/domain. Option (a) is simpler and correct under the rules —MassImportServicecreates Documents; the calls toPersonServiceandTagServiceare already going through service interfaces.👨💻 Felix Brandt — Senior Fullstack Developer
Observations
src/test/java/.../controller/,.../service/,.../model/,.../dto/has to move and update itspackagedeclaration. The test side is the hidden work here.DocumentServiceTest,DocumentControllerTest,PersonServiceIntegrationTestetc. are intest/service/andtest/controller/— not co-located with their production code. After REFACTOR-1, the target should be co-location:document/DocumentServiceTest.javanext todocument/DocumentService.java. This is the right time to do it, and skipping it means losing the legibility benefit for tests.lib/components/. Thedocument/andchronik/subdirectories already exist as working patterns. The work is moving those 81 flat components into their domain subdirectories and updating allimportstatements inroutes/**. A global search-and-replace is safe since all imports use the$lib/components/ComponentNameform.eslint.config.jshas no import-boundary rules. Theeslint-plugin-importor a customno-restricted-importsrule can enforcelib/components/document/*not being imported fromlib/components/person/*. However, this is harder to configure precisely than ArchUnit. A simpler approach: use ESLintno-restricted-importswith per-domain deny-lists targeting the specific known violations rather than a blanket rule.Recommendations
mvnpackage renames) rather than manualsed. Each class must get itspackagedeclaration updated and every import referencing it updated. IntelliJ handles this automatically and is safer than a find-replace script.document/*classes, next commit movesperson/*, etc.) rather than one giant commit. Smaller commits are easier to review and easier to bisect if something breaks. The test suite gates each commit.eslint-plugin-importno-restricted-pathsrule to preventlib/components/personDomain/*importing fromlib/components/documentDomain/*. This is simpler than a full custom plugin and can be extended incrementally as new domains stabilize.lib/components/[domain]/orlib/[domain]/components/? The issue sayslib/shared/for cross-cutting — clarify whether the domain components live atlib/components/document/(shallow) orlib/document/components/(deep). Shallow is already the established pattern (lib/components/document/exists) — stick with it for consistency.Open Decisions
document/DocumentServiceTest.java), or stay in a parallel test package tree? Co-location is cleaner, but it is additional scope. The issue doesn't specify. This should be decided before starting so the PR doesn't turn into a partial migration.🔒 Nora "NullX" Steiner — Application Security Engineer
Observations
SecurityConfig,PermissionAspect, and@RequirePermissionare moved.SecurityConfigandPermissionAspectmust land inshared/security/— not in any domain package. If they end up in, say,user/for convenience, any import fromuser/package becomes a transitive dependency for every domain that uses@RequirePermission. That's incorrect coupling.@RequirePermissionandPermissionenum are referenced by every controller. They must be inshared/security/. Their package change means every controller's import block changes — confirm the security annotations still resolve correctly after the package rename.SecurityConfigcurrently lives inconfig/— it referencesCustomUserDetailsServicewhich lives inservice/. After refactoring:CustomUserDetailsServiceshould move touser/(it's user-domain logic), andSecurityConfiginshared/security/will need to import fromuser/. This is a clean direction but requires explicit attention.AuthE2EControlleris a controller that exists only for E2E test auth flows. It must stay behind a profile guard (@Profile("e2e")or equivalent). Confirm this guard is not accidentally removed during the refactor. It currently lives incontroller/— moving it toauth/oruser/domain is correct, but double-check the profile annotation survives.RateLimitInterceptorinconfig/is a security control. It should move toshared/security/orshared/config/to signal its security intent, not get buried in a domain package.Recommendations
SecurityConfig → shared/security/,PermissionAspect → shared/security/,Permission → shared/security/,RateLimitInterceptor → shared/security/,CustomUserDetailsService → user/,AuthController → auth/ or user/,AuthE2EController → user/ with @Profile("e2e") confirmed.@WebMvcTestslice test after the refactor that verifies@RequirePermission(Permission.WRITE_ALL)is still enforced on a write endpoint. This is a 5-line test and confirms the AOP wiring survived the package move. Specifically: import the post-moveSecurityConfigandPermissionAspectin the test's@Import({...})and verify a READ_ALL user gets 403 on a DELETE endpoint.GlobalExceptionHandlerinto any domain. It belongs inshared/and mapsDomainException→ HTTP responses for the entire application. Moving it into, say,document/would be semantically wrong.shared/exports should be accessible cross-domain. This isn't a common violation today but the rule should prevent future drift.No critical security vulnerabilities are introduced by this refactor if the above is followed. No open decisions from security — the placements are clear.
🧪 Sara Holt — QA Engineer & Test Strategist
Observations
import org.raddatz.familienarchiv.controller.*,.service.*,.model.*,.dto.*— every single import must be updated after the package moves.ApplicationContextTest.javais the critical smoke test: it loads the full Spring context and will immediately catch any broken bean wiring caused by package moves. Run this after every domain move during REFACTOR-1.PostgresContainerConfig.javais in the test root and is imported by integration tests. It must not get lost or accidentally re-packaged during the refactor — it's shared test infrastructure../mvnw testafter each domain sub-move is the only way to catch breakage early.document/repository/exists. Confirming: ArchUnit must be written against the post-refactor package layout, not the current one.SenderModelServiceimportsTranscriptionBlockRepository(ocr→transcription cross-domain),SegmentationTrainingExportServiceimportsDocumentRepositoryandAnnotationRepository(ocr→document, ocr→annotation). These violations must be resolved before REFACTOR-3's test can be written green. If they are not resolved, REFACTOR-3 either gets a permanent@ignorelist (bad) or the test is intentionally weaker than advertised.Recommendations
ignoreClassesorignoreDependenciesexceptions)." Without this, the ArchUnit test can be written with a long exception list and still "pass."./mvnw testmust pass (REFACTOR-1) andnpm run test && npx playwright testmust pass (REFACTOR-2). This is already in the acceptance criteria but should be a CI gate, not a manual check.DocumentService.javatodocument/in one commit andDocumentServiceTest.javatodocument/in the next creates a window where the test doesn't compile. Keep them together.Open Decisions
ApplicationContextTestbe expanded to assert bean count or specific critical beans? Right now it just loads the context. A more robust smoke test would assert thatSecurityConfig,PermissionAspect, and each domain controller bean are present. This is optional scope but would make the safety net stronger.🚀 Tobias Wendt — DevOps & Platform Engineer
Observations
pom.xmlhas JaCoCo but no ArchUnit. ArchUnit being added in REFACTOR-3 is a new test dependency — it needs a Maven dependency entry../mvnw clean compile -DskipTests) is the fastest failure signal before running the full test suite.npm run check(svelte-check) will catch broken import paths immediately. Runningnpm run checkafter each domain move is faster feedback than a full build.eslint-plugin-importmust be added tofrontend/package.jsonif not already present (it isn't currently in the ESLint config). Alternatively,no-restricted-importsin the existing config needs no new dependency.Recommendations
./mvnw clean compile -DskipTests) that runs before the full test suite in REFACTOR-1's PR. Fast failure is better than a 2-minute wait.no-restricted-importswith per-domain deny lists in the existingeslint.config.js. No new package to maintain:pom.xmland add it to any Renovate/Dependabot config so patch updates are automated. ArchUnit is actively maintained and follows semantic versioning.No infrastructure changes needed, no open decisions from the DevOps side. This is a safe refactor from a deployment perspective.
📋 Elicit (Requirements Engineer)
Observations
Recommendations
SenderModelService→TranscriptionBlockRepository,SegmentationTrainingExportService→DocumentRepository+AnnotationRepository+TranscriptionBlockRepository.@ArchIgnore,ignoreDependency(), ororShould()clauses in the ArchUnit test that exist only to paper over known violations."No open decisions from the requirements perspective — the gaps above are clarification items that belong in this issue's body, not implementation choices.
🎨 Leonie Voss — UX Designer & Accessibility Strategist
Observations
lib/components/into domain subdirectories. This does not affect rendered HTML, CSS classes, or Tailwind tokens — only import paths change. Accessibility behavior is fully preserved.lib/components/root include several that I'd expect to be shared across domains:BackButton.svelte,ConfirmDialog.svelte,DateInput.svelte,GroupDivider.svelte,HelpPopover.svelte,LanguageSwitcher.svelte. These should land inlib/components/shared/orlib/shared/components/— not in any single domain folder. IfBackButton.sveltegets moved todocument/because a document page uses it, it becomes unreachable fromperson/pages without a cross-domain import. Plan theshared/component list explicitly before REFACTOR-2 starts.Recommendations
Component → Target domain → Reason. Any component used by pages in 2+ domains goes intoshared/. This prevents the need to move things twice.No open decisions from the UX/accessibility perspective.
🗳️ Decision Queue — Action Required
3 decisions need your input before implementation starts.
Architecture
Where does
MassImportServicelive after the refactor? It creates Documents but callsPersonService,TagService, and usesDocumentRepository. Options: (a)document/domain — it creates Documents, and its calls toPersonService/TagServicealready go through service interfaces so no boundary violation; (b)import/domain — avoids coupling todocument/but adds a new domain for a single service. Recommendation: option (a) — the primary output is a Document. (Raised by: Markus)Test co-location in REFACTOR-1: should test files move into domain packages alongside production code (e.g.
document/DocumentServiceTest.javanext todocument/DocumentService.java), or stay in a paralleltest/...package tree mirroring the new domain structure? Co-location is the cleaner end state and aligns with the legibility goal. But it doubles the number of file moves. Options: (a) full co-location in same PR as REFACTOR-1; (b) co-location as a separate follow-up after the structural move is merged. Either way, the convention must be decided before REFACTOR-1 starts so the PR isn't a partial migration. (Raised by: Felix, Sara)Quality Gates
ApplicationContextTestbe expanded to assert critical bean presence? Currently it only loads the Spring context and verifies no startup exception. Options: (a) keep it as a minimal smoke test — fast, low maintenance; (b) expand to assert thatSecurityConfig,PermissionAspect, and each domain's controller bean are present in the context — stronger safety net against silent misconfiguration after the package moves. Option (b) adds ~10 lines and makes the safety net meaningful. (Raised by: Sara)