Application platform¶
Status: Locked for implementation Reference: Fordsworth
fordsflexapplicationpackage, adapted for Quarkus 3.38 and RFC 7807
Every business component (registry, strata, …) follows these platform rules. The platform
component itself supplies cross-cutting infrastructure.
Layering (BCE)¶
| Layer | Package suffix | Responsibility |
|---|---|---|
boundary |
.boundary |
JAX-RS resources — thin implementations only |
boundary.api |
.boundary.api |
OpenAPI-annotated API interfaces; resources implement these |
control |
.control |
Procedures, repositories, transactions, and platform cross-cutting (error mapping, current user) |
entity |
.entity |
JPA entities, embeddables, enums, DTO records |
Do not add *Service classes. Name controls after the procedure they perform (PartyRegistration,
LicenseImport, SubmissionCertification).
Boundaries delegate to control. Domain controls never assemble HTTP responses; they throw
KoventException. Platform control hosts application-wide exception mappers and the current-user
filter so resources stay clean.
REST paths¶
Global prefix is quarkus.rest.path=/api in application.properties. API interfaces declare
relative paths only (@Path("/parties"), not @Path("/api/parties")).
Each business component exposes a boundary.api interface with OpenAPI annotations
(@Operation, @APIResponse, @Tag). The resource in boundary implements the interface and
contains no path or documentation annotations — only delegation to control.
// boundary.api — contract + OpenAPI
@Path("/parties")
@Tag(name = "Parties")
public interface PartiesApi {
@GET
@Path("/{partyId}")
@Operation(operationId = "getParty", summary = "Party profile with licenses")
PartyProfile getParty(@RestPath UUID partyId);
}
// boundary — implementation only
@ApplicationScoped
public class PartiesResource implements PartiesApi {
public PartyProfile getParty(UUID partyId) {
return loadPartyProfile.require(partyId);
}
}
Dependency injection¶
Constructor injection only. No @Inject on fields. CDI resolves a single public constructor
automatically.
Persistence — repository pattern¶
Entities are plain JPA @Entity / @MappedSuperclass types. They do not extend PanacheEntity
(active record).
Persistence lives in control:
@ApplicationScoped
public class PartyRepository implements PanacheRepository<Party> {
public Optional<Party> findByRegisteredNumber(String registeredNumber) {
return find("organisation.registeredNumber", registeredNumber).firstResultOptional();
}
}
Only repositories call persist, delete, and query methods. Controls call repositories.
All persisted entities extend AuditableEntity (UUID id, embedded Audit).
Audit and current user¶
| Type | Role |
|---|---|
Audit |
created_by, created_on, updated_by, updated_on |
AuditableEntity |
@PrePersist / @PreUpdate stamp audit from CurrentUserHolder |
CurrentUser |
Request-scoped — reads SecurityIdentity for control-layer code |
CurrentUserRequestFilter |
Binds principal to CurrentUserHolder per request |
Domain status history (for example license_register_status_history) is separate from row audit.
Errors — RFC 7807 Problem Details¶
Application code throws KoventException with a KoventErrorCode. Boundaries do not build HTTP
responses for business failures. Responses use ProblemDetailBody (RFC 7807 wire format). Jakarta
WS RS 3.1 does not yet ship jakarta.ws.rs.core.ProblemDetail on the Quarkus classpath; the shape
is identical and can be swapped later or replaced with quarkus-http-problem if desired.
| Component | Role |
|---|---|
KoventErrorCode |
Enum mapping to HTTP status + title |
KoventException |
Domain failure with optional parameters |
ProblemDetailBody |
RFC 7807 response body (application/problem+json) |
ProblemResponses |
Builds problem bodies from domain exceptions |
KoventExceptionMapper |
Maps KoventException → application/problem+json |
ConstraintViolationExceptionMapper |
Bean validation failures |
FallbackExceptionMapper |
Unhandled throwables; logs server errors |
Example response:
{
"type": "about:blank",
"title": "Not found",
"status": 404,
"detail": "Not found: party, p-1",
"code": "NOT_FOUND",
"parameters": ["party", "p-1"]
}
Control example:
return repository
.findByIdOptional(id)
.orElseThrow(() -> new KoventException(KoventErrorCode.NOT_FOUND, "party", id));
Transactions¶
@Transactional on control methods that mutate state. Boundaries stay non-transactional.
Diagrams¶
Entity-relationship diagrams for domain models are maintained in draw.io under
docs/diagrams/ — not inline Mermaid ER blocks.
Architecture tests (ArchUnit)¶
BCE boundaries are enforced in CI via ArchitectureTest (src/test/java/.../architecture/).
Violations fail mvn test. Rules include:
| Rule | Intent |
|---|---|
| Layer access | Boundary → control/entity; control → entity (+ shared platform.control); entity → entity only |
| No BC cycles | Slices (registry, platform, …) must not circularly depend |
| No dumping grounds | No shared, common, util, application, exceptions packages |
JAX-RS @Path |
Only in ..boundary.. (including boundary.api) |
| Exception mappers | Only in platform.control |
| API pattern | *Resource implements a boundary.api.*Api interface |
No *Service |
Controls named after procedures, not generic services |
| Repositories | *Repository classes live in control |
| No active record | Entities do not extend PanacheEntity |
| Constructor injection | No @Inject on fields |
Reference: Quarkus + ArchUnit BCE tutorial.