Engineering · Java 21 · Spring Boot 3.5

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

Feature packages
28
under com.cplk.api
@Entity classes
52
@RestController
36
@Service classes
69
Flyway migrations
142
ArchUnit rules
16
Tests
1,247+
94.9% coverage
State machines
3
Property, Article, Blog

Package layout

Code is organised by business domain, not by technical layer. Each feature package owns its controllers, services, repositories, DTOs, mappers and tests.

mindmap root((com.cplk.api)) Identity security user onboarding preregister verification Tenants agency tenant Listings property lookup location favorite contact contactUs inquiry Money subscription payment financial boost Content blog article analytics dashboard Platform audit notification support system storage ratelimit scheduler email Foundations common config db
28 packages, four functional clusters and one foundation cluster.

Package catalogue

PackageOwnsNotable artefacts
propertyProperty aggregate · workflow · images · payment slip · refundsPropertyService, PropertyWorkflowService, PropertyApprovalService, PropertyImageUploadService, PropertyCodeGenerator
agencyTenants, agency users, brandingAgencyService, AgencyUserService, KeycloakOrganizationBackfill
subscriptionPackages, agency & user subs, creditsSubscriptionService, SubscriptionLifecycleService, PackageService, PointsTransaction
paymentPayHere checkout, webhooks, recurring, invoicesPaymentService, PayHereRecurringService, PayHereOAuthService, InvoicePdfService
boostProperty visibility boostsBoostService
financialImmutable financial ledger & summariesFinancialAuditService, TransactionSummaryService
inquiryPublic/lead intake + threaded messagesInquiryService, InquiryMessage
blog · articleContent with draft/review/publish workflowBlogWorkflowService, ArticleWorkflowService
notificationIn-app, email, SSE, RabbitMQ consumersNotificationService, RedisSseNotificationBroadcaster, DlqNotificationHandler
storageR2 / S3 / local + image processingR2StorageService, LocalStorageService, ImageProcessingService
securityAuth, JWT, Google One Tap, reCAPTCHAAuthService, KeycloakService, RecaptchaService
tenantMulti-tenancy enforcementTenantFilter, TenantFilterAspect, BypassTenantFilter
auditAudit log, login historyAuditService, LoginHistoryService
ratelimitBucket4j filter, per-endpoint configRateLimitFilter
schedulerShedLock-wrapped jobsrecurring charges, cleanup, expiry sweeps
commonBase classes, ApiResponse, encryptionBaseEntity, ImmutableEntity, EncryptedStringConverter, PiiEncryptionService
configSecurityConfig, state machines, cachesSecurityConfig, *StateMachineConfig, NotificationAsyncConfig

Layers & responsibilities

flowchart TB classDef ctrl fill:#ecfeff,stroke:#0891b2,color:#0f172a classDef svc fill:#eef2ff,stroke:#1e3a5f,color:#0f172a classDef repo fill:#fff7ed,stroke:#f97316,color:#0f172a classDef ent fill:#f0fdf4,stroke:#16a34a,color:#0f172a classDef cross fill:#fef2f2,stroke:#dc2626,color:#0f172a UI["UI / external caller"] CTL["@RestController
· @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
Strict downward dependencies. ArchUnit forbids the reverse: services cannot depend on controllers, repositories cannot depend on services.

Controller surface

36 controllers, all returning ResponseEntity<ApiResponse<T>> (or PagedResponse<T> wrapped in it). Grouped by area:

AreaControllersBase paths
AuthAuthController, SyncController/auth/**
PropertiesPropertyController, PropertyPaymentSlipController, PropertyRefundController/properties/**
AgenciesAgencyController, AgencyUserController/agencies/**
UsersUserController/users/**
BillingSubscriptionController, PackageController, PaymentController, InvoiceController, BoostController/subscriptions, /packages, /payments, /invoices, /boosts
FinancialFinancialAuditController, TransactionSummaryController/financial-audit, /transaction-summary
EngagementInquiryController, ContactController, FavoriteController, ContactUsController/inquiries, /contacts, /favorites, /contact-us
ContentBlogController, BlogImageController, ArticleController, ArticleCategoryController/blogs, /articles
NotificationsNotificationController, NotificationPreferenceController, SystemAnnouncementController/notifications, /notification-preferences, /announcements
AdminSystemConfigController, PublicSystemConfigController, AdminVerificationController, VerificationController, SupportTicketControllervarious
Lookup & storageLookupController, LocationController, FileUploadController, OnboardingController, PreRegisterController/lookup, /locations, /uploads, /onboarding
InsightsAnalyticsController, 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:

  1. Layer dependencies — services↛controllers, repos↛services, controllers↛repos.
  2. Controller shape@RestController, returns ResponseEntity<ApiResponse<T>>; @RequestBody must have @Valid.
  3. Repository shape — interfaces extending JpaRepository.
  4. Service shape@Service (exceptions: schedulers, generators, listeners); no HTTP layer access.
  5. Injection — constructor injection only via Lombok @RequiredArgsConstructor.
  6. Entity table — every @Entity has @Table.
  7. Entity inheritance — must extend BaseEntity / ImmutableEntity (whitelist: SystemConfig, projection views, PendingFileKey).
  8. Naming*Controller, *Service, *Repository; DTOs end in Request, Response, Dto.
  9. Exception types — no new RuntimeException(msg); use typed app exceptions.
  10. Logging — no System.out; SLF4J only.
  11. Package cycles — cycle-free except a small whitelist (property↔subscription, payment↔subscription, agency↔user).
  12. ApiResponse wrapping — every controller method (exceptions: PayHere webhook returning plain string, bulk import endpoints).
  13. Paged endpointsApiResponse<PagedResponse<T>>; never raw Page<T>.
  14. Entity leakage — controllers must use DTOs (exception: @CurrentUser User).
Run separately for fast feedback: ./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

  1. Write the controller test first (TDD).
  2. Create the request/response DTOs in feature/dto/.
  3. Add the controller method returning ResponseEntity<ApiResponse<...>>.
  4. Implement the service method with @Transactional + audit call.
  5. Add repository query if needed.
  6. Update frontend/src/types/api.ts and the relevant service.
  7. Run ./gradlew check — ArchUnit will fail if anything's off.

Dependency highlights

GroupLibraries
Spring Boot 3.5web, security, oauth2-resource-server, data-jpa, mail, validation, cache, amqp, data-redis
Persistencepostgresql, flyway-core, flyway-database-postgresql, hypersistence-utils-hibernate-63 (JSONB)
Identityspring-security-oauth2-jose, keycloak-admin-client 26.0.5
Caching & throttlingcaffeine 3.1, bucket4j-core 8.7
Mapping & PDFmapstruct 1.5, pdfbox 3.0
Imagesthumbnailator 0.4, imageio-webp 3.10, metadata-extractor 2.19
Schedulingshedlock-spring 5.13 + JDBC template provider
Security & loggingowasp-java-html-sanitizer, logstash-logback-encoder 7.4
Cloud / S3software.amazon.awssdk BOM 2.21
Docsspringdoc-openapi-starter-webmvc-ui 2.8 (off by default in prod)
TestsJUnit 5, spring-boot-starter-test, spring-security-test, testcontainers-postgresql 1.19, archunit-junit5 1.3