Backend engineering.
How code is organised, what conventions hold, what gets enforced at build time.
Everything here is grounded in the source under
backend/src/main/java/com/cplk/api/.
Snapshot
com.cplk.apiPackage layout
Code is organised by business domain, not by technical layer. Each feature package owns its controllers, services, repositories, DTOs, mappers and tests.
Package catalogue
| Package | Owns | Notable artefacts |
|---|---|---|
property | Property aggregate · workflow · images · payment slip · refunds | PropertyService, PropertyWorkflowService, PropertyApprovalService, PropertyImageUploadService, PropertyCodeGenerator |
agency | Tenants, agency users, branding | AgencyService, AgencyUserService, KeycloakOrganizationBackfill |
subscription | Packages, agency & user subs, credits | SubscriptionService, SubscriptionLifecycleService, PackageService, PointsTransaction |
payment | PayHere checkout, webhooks, recurring, invoices | PaymentService, PayHereRecurringService, PayHereOAuthService, InvoicePdfService |
boost | Property visibility boosts | BoostService |
financial | Immutable financial ledger & summaries | FinancialAuditService, TransactionSummaryService |
inquiry | Public/lead intake + threaded messages | InquiryService, InquiryMessage |
blog · article | Content with draft/review/publish workflow | BlogWorkflowService, ArticleWorkflowService |
notification | In-app, email, SSE, RabbitMQ consumers | NotificationService, RedisSseNotificationBroadcaster, DlqNotificationHandler |
storage | R2 / S3 / local + image processing | R2StorageService, LocalStorageService, ImageProcessingService |
security | Auth, JWT, Google One Tap, reCAPTCHA | AuthService, KeycloakService, RecaptchaService |
tenant | Multi-tenancy enforcement | TenantFilter, TenantFilterAspect, BypassTenantFilter |
audit | Audit log, login history | AuditService, LoginHistoryService |
ratelimit | Bucket4j filter, per-endpoint config | RateLimitFilter |
scheduler | ShedLock-wrapped jobs | recurring charges, cleanup, expiry sweeps |
common | Base classes, ApiResponse, encryption | BaseEntity, ImmutableEntity, EncryptedStringConverter, PiiEncryptionService |
config | SecurityConfig, state machines, caches | SecurityConfig, *StateMachineConfig, NotificationAsyncConfig |
Layers & responsibilities
· @PreAuthorize
· @Valid DTO
· returns ApiResponse<T>"]:::ctrl SVC["@Service
· @Transactional
· business rules · state machines"]:::svc REPO["JpaRepository
· Spring Data queries
· wrapped by TenantFilterAspect"]:::repo ENT["@Entity
· extends BaseEntity / TenantAwareEntity"]:::ent PG[("PostgreSQL")] CC["common · config · tenant · security · audit"]:::cross UI --> CTL --> SVC --> REPO --> PG REPO --> ENT CC -.cross-cutting.-> CTL CC -.cross-cutting.-> SVC CC -.cross-cutting.-> REPO
Controller surface
36 controllers, all returning ResponseEntity<ApiResponse<T>> (or PagedResponse<T> wrapped in it). Grouped by area:
| Area | Controllers | Base paths |
|---|---|---|
| Auth | AuthController, SyncController | /auth/** |
| Properties | PropertyController, PropertyPaymentSlipController, PropertyRefundController | /properties/** |
| Agencies | AgencyController, AgencyUserController | /agencies/** |
| Users | UserController | /users/** |
| Billing | SubscriptionController, PackageController, PaymentController, InvoiceController, BoostController | /subscriptions, /packages, /payments, /invoices, /boosts |
| Financial | FinancialAuditController, TransactionSummaryController | /financial-audit, /transaction-summary |
| Engagement | InquiryController, ContactController, FavoriteController, ContactUsController | /inquiries, /contacts, /favorites, /contact-us |
| Content | BlogController, BlogImageController, ArticleController, ArticleCategoryController | /blogs, /articles |
| Notifications | NotificationController, NotificationPreferenceController, SystemAnnouncementController | /notifications, /notification-preferences, /announcements |
| Admin | SystemConfigController, PublicSystemConfigController, AdminVerificationController, VerificationController, SupportTicketController | various |
| Lookup & storage | LookupController, LocationController, FileUploadController, OnboardingController, PreRegisterController | /lookup, /locations, /uploads, /onboarding |
| Insights | AnalyticsController, DashboardController, AuditController | /analytics, /dashboard, /audit |
Conventions
ApiResponse envelope
Every controller method returns the same envelope shape; clients can rely on it:
// 200 OK
{ "success": true, "data": { ... }, "message": null, "timestamp": "2026-05-13T08:21:14Z" }
// 4xx / 5xx
{ "success": false, "data": null,
"error": { "code": "PROPERTY_NOT_FOUND", "message": "...",
"fields": { "id": "must not be null" } },
"timestamp": "..." }
Pagination
@GetMapping
public ResponseEntity<ApiResponse<PagedResponse<PropertyResponse>>> list(
@ParameterObject PropertyFilter filter,
@PageableDefault(size = 20, sort = "createdAt", direction = DESC) Pageable pageable
) { ... }
Returned PagedResponse carries content, page, size, totalElements, totalPages, hasNext.
UUID vs slug
/api/properties/{uuid}— internal/portal use/api/properties/slug/{slug}— SEO/public use- Entities without slugs (User, Payment, Subscription) are UUID-only
- Nested:
/api/agencies/{agencyId}/users/{userId}
State machines
Property
PropertyStateMachineConfig.java
DRAFT → PENDING_REVIEW → ACTIVE → SOLD/LEASED/INACTIVE → REFUND_REJECTED. Notifier: PropertyTransitionNotifier.
Article
ArticleStateMachineConfig.java
DRAFT → PENDING_REVIEW → PUBLISHED → ARCHIVED. Approval gated to SUPER_ADMIN.
Blog
BlogStateMachineConfig.java
Same shape as articles; supports drafts on published via hasPendingDraft.
ArchUnit rules
Rules live in backend/src/test/java/com/cplk/api/architecture/ArchitectureTest.java. Highlights:
- Layer dependencies — services↛controllers, repos↛services, controllers↛repos.
- Controller shape —
@RestController, returnsResponseEntity<ApiResponse<T>>;@RequestBodymust have@Valid. - Repository shape — interfaces extending
JpaRepository. - Service shape —
@Service(exceptions: schedulers, generators, listeners); no HTTP layer access. - Injection — constructor injection only via Lombok
@RequiredArgsConstructor. - Entity table — every
@Entityhas@Table. - Entity inheritance — must extend
BaseEntity/ImmutableEntity(whitelist:SystemConfig, projection views,PendingFileKey). - Naming —
*Controller,*Service,*Repository; DTOs end inRequest,Response,Dto. - Exception types — no
new RuntimeException(msg); use typed app exceptions. - Logging — no
System.out; SLF4J only. - Package cycles — cycle-free except a small whitelist (property↔subscription, payment↔subscription, agency↔user).
- ApiResponse wrapping — every controller method (exceptions: PayHere webhook returning plain string, bulk import endpoints).
- Paged endpoints —
ApiResponse<PagedResponse<T>>; never rawPage<T>. - Entity leakage — controllers must use DTOs (exception:
@CurrentUser User).
./gradlew test --tests "com.cplk.api.architecture.ArchitectureTest"
Testing
Pyramid
- ~90 unit tests — services, mappers, utils.
- 3 integration tests tagged
@Tag("integration"):TenantIsolationIntegrationTest,PaymentIdempotencyIntegrationTest,CascadeDeleteIntegrationTest. - ArchUnit suite as a structural fence.
- Testcontainers PostgreSQL 1.19.8 boots for integration tests.
Coverage gate
JaCoCo 80% minimum, current 94.9%.
Excluded from coverage: DTOs, entities, configs, controllers (E2E covers them), external SDK wrappers (S3, R2, Local storage), Keycloak admin wrapper, mail, request/rate filters.
Cheatsheet
Build & run
# build
cd backend && ./gradlew build
# run local profile
cd backend && ./gradlew bootRun --args='--spring.profiles.active=local'
# tests with coverage
ulimit -n 8192 && cd backend && ./gradlew test jacocoTestReport
# enforce coverage gate
cd backend && ./gradlew check
Add a new endpoint
- Write the controller test first (TDD).
- Create the request/response DTOs in
feature/dto/. - Add the controller method returning
ResponseEntity<ApiResponse<...>>. - Implement the service method with
@Transactional+ audit call. - Add repository query if needed.
- Update
frontend/src/types/api.tsand the relevant service. - Run
./gradlew check— ArchUnit will fail if anything's off.
Dependency highlights
| Group | Libraries |
|---|---|
| Spring Boot 3.5 | web, security, oauth2-resource-server, data-jpa, mail, validation, cache, amqp, data-redis |
| Persistence | postgresql, flyway-core, flyway-database-postgresql, hypersistence-utils-hibernate-63 (JSONB) |
| Identity | spring-security-oauth2-jose, keycloak-admin-client 26.0.5 |
| Caching & throttling | caffeine 3.1, bucket4j-core 8.7 |
| Mapping & PDF | mapstruct 1.5, pdfbox 3.0 |
| Images | thumbnailator 0.4, imageio-webp 3.10, metadata-extractor 2.19 |
| Scheduling | shedlock-spring 5.13 + JDBC template provider |
| Security & logging | owasp-java-html-sanitizer, logstash-logback-encoder 7.4 |
| Cloud / S3 | software.amazon.awssdk BOM 2.21 |
| Docs | springdoc-openapi-starter-webmvc-ui 2.8 (off by default in prod) |
| Tests | JUnit 5, spring-boot-starter-test, spring-security-test, testcontainers-postgresql 1.19, archunit-junit5 1.3 |