nyazul@portfolio:~$ ./boot.sh

Nyazul Ansari

Backend Engineer — Java / Spring Boot

000%

← All projects

Backend · Multi-tenant · AWS S3 · 2025 – 2026

RealEstate CRM

A multi-tenant CRM for property developers: projects broken down to individual flats, the enquiry-to-booking pipeline, disbursements, and project documents kept private in S3.

Role
Backend engineer on a two-person team, YSM Infosolution
Stack
Java 21Spring Boot 3Spring SecurityPostgreSQLAWS S3React 18DockerNginx

5

Hierarchy levels

17

REST controllers

10 min

Download-link lifetime

Company project: the code is private, so this page covers architecture and behaviour rather than source.

01 / The challenge

A deep, shared hierarchy

Everything hangs off an organization's projects: wings, floors and flats, plus enquiries, follow-ups, bookings, clients and the bank disbursement schedule.

Private files, easy access

Agreements, IDs and letterheads must never sit in a public bucket, but staff still need to view and share them in a click.

Two systems, one operation

Creating a project writes dozens of rows and uploads files. The database can roll back; S3 can't.

02 / Architecture journey

Follow a project from creation to a buyer's booking, and the files that travel with it.

01 / 06 · One origin, no CORS

  1. 01

    One origin, no CORS

    The React SPA is served by Nginx, which also proxies /api/* to the Spring Boot container. The browser only ever talks to one origin.

    Security is stateless JWT with access and refresh tokens, and endpoints are gated by role. Every authenticated user carries their organization id, which the services use to scope data to that tenant.

  2. 02

    Create a project in one request

    An admin submits the whole project at once: details, a MahaRERA number, wings with their floors and flats, approved banks, amenities, documents and the disbursement schedule. Project names and MahaRERA numbers are checked for uniqueness first.

    It all runs in one @Transactional method, so the rows commit together or not at all.

  3. 03

    Files go to S3, keys go to the database

    The letterhead and project documents upload to a private bucket under {orgId}/{projectName}/LetterHead/ and /Documents/. Only the object key is stored in PostgreSQL, never bytes or a public URL.

    Because the uploads happen mid-transaction and S3 can't roll back, a sub-step that reports failure triggers a compensating delete: list everything under the project's prefix, batch-delete it, then throw so the database rolls back too.

  4. 04

    Short-lived links instead of a public bucket

    Viewing or sharing a document asks the API for a presigned GET URL for that one object, valid for ten minutes. Access control stays in the application; the bucket stays private.

  5. 05

    From enquiry to booking

    Sales staff log enquiries against a project and track them through follow-ups. A booking ties three things together: the enquiry it came from, the specific flat, and the client buying it.

  6. 06

    A to-do list that writes itself

    At 01:00 every day a scheduler generates the follow-up tasks due, so each employee starts the morning with a current queue instead of yesterday's leftovers.

03 / System components

ProjectServiceImpl
Creates a project and its whole hierarchy in one transaction.
hardDeleteProjectRecursive
Compensation: S3 prefix delete plus rows, used to undo a failed create.
S3StorageService
Upload, download and batch prefix delete with AWS SDK v2.
S3PresignedUrlService
Single-object GET links that expire after 10 minutes.
DailyTaskScheduler
Generates the day's follow-up tasks at 01:00.
Nginx
Serves the SPA and proxies /api to the backend container.
JWT security
Stateless access + refresh tokens, role-gated endpoints.
Docker Compose
PostgreSQL, backend and the Nginx frontend as one stack.

04 / Now you drive it

Step 3's failure handling, replayed. Make any step fail, either by reporting failure or by throwing, and compare what's left in the database and in the bucket.

One transaction, two systemscreateProject() · PostgreSQL + S3
PostgreSQLempty
  • // no rows
S3 · 7f3a9c/Skyline Heights/not transactional
  • // no objects
  1. // Pick a failure, then send the request.

05 / What I'd change next

Compensate on exceptions too

The clean-up only runs when a step returns false. An exception skips it, so files uploaded earlier in the request are left in S3. Registering the S3 clean-up as an after-rollback hook would cover both paths.

Page through large prefixes

A single listObjectsV2 call returns at most 1,000 keys, and the prefix delete reads one page. A project with more files would be only partly cleaned.

Key objects by id, not name

Keys use the project's name. Renaming a project would separate it from its files; a stable project id in the prefix wouldn't.

06 / Deep dives from this project