VersionVault is a Git-like versioned file storage backend built with Java, Spring Boot, PostgreSQL, and content-addressed object storage.
It models the core mechanics behind a version control system: repositories, branches, mutable working trees, immutable commits, manifests, historical file retrieval, commit diffs, merge-base detection, three-way merges, and reachability-based garbage collection.
- Features
- Architecture
- Core Data Model
- Commit & Branch Model
- Content-Addressed Object Storage
- Branching
- Commit History
- Diffs
- Three-Way Merge
- Merge-Base Detection
- Garbage Collection
- Optimistic Concurrency Control
- Storage Backends
- Authentication & Authorization
- API Documentation
- Tech Stack
- Running Locally
- Local Filesystem Storage
- Using AWS S3 Storage
- Configuration
- Upload Limits
- Project Structure
- Example Workflow
- Engineering Decisions
- Known Limitations
For detailed architecture and flow diagrams, see the VersionVault Architecture & Diagrams.
- Repository and branch management
- Independent working trees per branch
- Immutable commit snapshots
- Parent and two-parent merge commits
- Commit history with cursor-based pagination
- Historical file listing and retrieval
- Commit-to-parent diffs
- Branch divergence and merge-base detection
- Fast-forward merges
- Three-way merges
- Conflict detection
- SHA-256 content-addressed storage
- Object deduplication
- Separate metadata and file-content storage
- Local filesystem storage
- AWS S3 storage
- Configurable storage backend through application configuration
- Path-safe local object storage
- JWT-based authentication
- Password hashing
- Authenticated write operations
- Repository ownership authorization
- System-administrator-only garbage collection
- Stateless Spring Security configuration
- Reachability-based object garbage collection
- Merge-aware commit graph traversal
- Grace-period protection for recently created objects
- Working-tree protection during garbage collection
- Dry-run garbage collection scan
- Dockerized Spring Boot application
- Multi-stage Docker build
- Docker Compose development environment
- PostgreSQL container
- Persistent PostgreSQL volume
- Persistent local object storage
- Environment-based configuration
- Flyway database migrations
- Configurable upload limits
At a high level, VersionVault separates repository metadata, mutable working state, immutable history, and binary object storage.
Client
|
v
┌─────────────────┐
│ Spring Boot │
│ REST API │
└────────┬────────┘
|
┌────────────────┼─────────────────┐
| | |
v v v
Repository/Branch Commit/Merge Object Service
Working Tree | |
| | v
| | ┌──────────────┐
| | │ ObjectStorage│
| | └──────┬───────┘
| | |
| | ┌───────┴───────┐
| | | |
v v v v
PostgreSQL Commit DAG Local S3
Storage Bucket
The application uses PostgreSQL for metadata and object references while the actual file bytes are stored through the ObjectStorage abstraction.
User
└── Repository
|
├── Branch
| |
│ └── WorkingEntry ──> Object
│
├── Commit
| |
│ └── Manifest
| |
│ └── ManifestEntry ──> Object
│
└── ...
Object
└── ObjectStorage
├── LocalObjectStorage
└── S3ObjectStorage
A branch's working tree is mutable:
Branch
|
└── Working Entries
|
├── README.md → Object A
├── src/App.java → Object B
└── config.yml → Object C
Creating a commit converts the current working state into an immutable manifest:
Working Tree
|
v
Manifest
|
v
Commit
|
└── parent → previous Commit
Later modifications affect the working tree and future manifests, not previous commits.
VersionVault does not store file bytes directly inside commits or manifests.
When content is uploaded:
File Content
|
v
SHA-256
|
v
Content Hash
|
v
ObjectEntity
|
└── storageKey
|
v
ObjectStorage
The database stores object metadata such as:
- Content hash
- Storage key
- Size
- Creation timestamp
The actual bytes are stored through the configured storage backend.
Because objects are identified by their SHA-256 content hash, identical content can be reused:
file A ─────┐
├──────→ Object X
file B ─────┘
This avoids storing the same file content multiple times.
Branches point to commits and maintain their own working trees.
When a branch is created from an existing commit, its working tree is initialized from that commit's manifest.
C2
/ \
/ \
main feature
| |
| └── Working Tree B
|
└── Working Tree A
The branches initially reference the same underlying objects, but subsequent working-tree changes remain independent.
This means creating a branch does not require copying the actual file contents.
Commits form a directed history graph.
Normal commits have one parent:
C1 → C2 → C3
Merge commits can have two parents:
C2
/ \
C3 C4
\ /
C5
For the merge commit:
C5.parent = C3
C5.secondParent = C4
The second parent allows the system to preserve both sides of a merge in the commit graph.
VersionVault computes commit diffs by comparing the manifests of a commit and its parent.
Files are classified as:
ADDED
MODIFIED
DELETED
The diff operates on file paths and object identities/content hashes rather than comparing raw file bytes.
For example:
Commit A Commit B
README.md → Object A README.md → Object B
config.yml → Object C config.yml → Object C
old.txt → Object D new.txt → Object E
The resulting changes can identify:
README.md → MODIFIED
old.txt → DELETED
new.txt → ADDED
VersionVault uses a three-way merge based on:
Base
/ \
Ours Theirs
For each path, the system compares the base, target, and source versions.
The merge handles cases such as:
- Only source changed → source version
- Only target changed → target version
- Both sides have the same version → shared version
- Neither side changed → base version
- Both sides changed differently → conflict
Supported merge outcomes include:
UP_TO_DATE
FAST_FORWARD
MERGED
CONFLICT
A successful non-fast-forward merge creates a merge commit with two parents.
Before performing a three-way merge, VersionVault determines the common ancestor of the two branch heads.
The commit graph can contain both normal parent edges and second-parent edges from merge commits.
C1
/ \
C2 C3
\ /
C4
The merge-base allows the system to determine which changes were introduced independently on each side.
Objects can become unreachable from the current branch history.
VersionVault uses reachability-based garbage collection:
Branch HEADs
|
v
Commit DAG
|
v
Manifests
|
v
Objects
An object is considered reachable when it is referenced by a manifest belonging to a commit reachable from a branch HEAD.
The traversal follows both:
parentCommit
secondParentCommit
so objects referenced by merge history are preserved.
Unreachable objects are not immediately deleted.
A configurable grace period protects recently created objects:
Object
|
├── Reachable → keep
|
└── Unreachable
|
├── Within grace period → protect
|
└── Past grace period
|
├── Used by working tree → protect
|
└── Otherwise → eligible for deletion
Garbage collection supports:
POST /gc/scan
for a dry-run reachability analysis and:
POST /gc/clean
for actual cleanup.
These operations are restricted to the system administrator.
Branch HEAD updates use an optimistic concurrency check.
Conceptually:
UPDATE branches
SET head_commit_id = :newHead
WHERE id = :branchId
AND head_commit_id = :expectedHead;If no row is updated, the branch HEAD changed after the operation read it.
This prevents concurrent commit operations from silently overwriting one another's branch HEAD.
This project keeps the concurrency model focused on branch-head consistency rather than attempting to provide full transactional coordination between every filesystem and database operation.
VersionVault separates object management from physical storage through:
ObjectStorageCurrent implementations:
ObjectStorage
|
├── LocalObjectStorage
|
└── S3ObjectStorage
The selected backend is controlled through configuration:
application:
storage:
type: localor:
application:
storage:
type: s3The same object-management logic works with either backend.
The API uses JWT-based authentication with Spring Security.
Authentication endpoints include:
POST /auth/register
POST /auth/authenticate
Authenticated operations use a bearer token:
Authorization: Bearer <JWT>
Repository mutations verify the authenticated user against repository ownership.
Garbage collection is restricted to the system administrator.
Swagger/OpenAPI documentation is available when the application is running.
http://localhost:8080/swagger-ui/index.html
OpenAPI JSON:
http://localhost:8080/api-docs
The project keeps internal/test-only endpoints available for development while hiding them from the public Swagger documentation.
- Java 21
- Spring Boot
- Spring Web
- Spring Data JPA
- Hibernate
- Spring Security
- Bean Validation
- Lombok
- PostgreSQL
- Flyway
- Local filesystem
- AWS S3
- AWS SDK for Java
- SHA-256 content addressing
- Springdoc OpenAPI / Swagger UI
- Docker
- Docker Compose
- Maven
- Postman
- Git
- IntelliJ IDEA
For the non-Docker setup:
- Java 21+
- PostgreSQL
- Maven
For the Docker setup:
- Docker
- Docker Compose
The recommended local setup is Docker Compose.
The Compose environment contains:
Docker Compose
│
├── PostgreSQL
│
└── VersionVault Backend
Create a .env file in the project root:
JWT_SECRET_KEY=your-secret-keyThe .env file should not be committed to Git.
From the project root:
docker compose up --buildThe backend will be available at:
http://localhost:8080
PostgreSQL runs on:
localhost:5432
The database is:
vault
Flyway automatically applies the database migrations when the application starts.
docker compose downThe PostgreSQL data is persisted through the Docker volume.
To intentionally remove the database volume and recreate the database from migrations:
docker compose down -v
docker compose up --buildThe Docker Compose configuration uses local object storage by default.
application:
storage:
type: localThe host directory:
./storage
is mounted into the backend container as:
/app/storage
Objects are stored under:
storage/objects/
The storage directory is persisted independently from the backend container.
VersionVault can use S3 instead of the local filesystem.
Set:
application:
storage:
type: s3
s3:
bucket: your-bucket-name
region: ap-south-1or configure the equivalent environment variables:
STORAGE_TYPE=s3
S3_BUCKET=your-bucket-name
AWS_REGION=ap-south-1AWS credentials are provided through the standard AWS SDK credential configuration.
No application code changes are required to switch from local storage to S3.
Important configuration values can be supplied through environment variables.
| Variable | Purpose | Default |
|---|---|---|
DB_URL |
PostgreSQL JDBC URL | jdbc:postgresql://localhost:5432/vault |
DB_USERNAME |
Database username | postgres |
DB_PASSWORD |
Database password | password |
STORAGE_TYPE |
local or s3 |
local |
STORAGE_ROOT |
Local object-storage root | ./storage |
S3_BUCKET |
S3 bucket name | — |
AWS_REGION |
AWS region | ap-south-1 |
JWT_SECRET_KEY |
JWT signing secret | — |
JWT_EXPIRATION |
JWT lifetime | 86400000 (1 day) |
REFRESH_TOKEN_EXPIRATION |
Refresh-token lifetime | 604800000 |
MAX_FILE_SIZE |
Maximum individual upload | 5MB |
MAX_REQUEST_SIZE |
Maximum multipart request | 6MB |
GC_GRACE_PERIOD_HOURS |
GC grace period | 1 |
For anything beyond local development, sensitive values such as database passwords and JWT secrets should be supplied through the deployment environment rather than committed configuration.
The application limits uploads to:
Maximum file size: 5 MB
Maximum request size: 6 MB
These values are configurable through:
MAX_FILE_SIZE
MAX_REQUEST_SIZEVersionVault/
│
├── backend/
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/com/version_vault/
│ │ │ │ ├── controller/
│ │ │ │ ├── service/
│ │ │ │ ├── models/
│ │ │ │ ├── repo/
│ │ │ │ ├── dtos/
│ │ │ │ ├── mapper/
│ │ │ │ ├── storage/
│ │ │ │ ├── security/
│ │ │ │ ├── configs/
│ │ │ │ ├── exceptions/
│ │ │ │ └── utils/
│ │ │ │
│ │ │ └── resources/
│ │ │ ├── application.yaml
│ │ │ └── db/migration/
│ │ │
│ │ └── test/
│ │
│ └── Dockerfile
│
├── postman/
│ └── postman_collection.json
│
├── storage/
│ └── objects/
│
├── docker-compose.yml
├── .env
└── README.md
A typical VersionVault workflow is:
Register / Authenticate
|
v
Create Repository
|
v
Create / use main branch
|
v
Add or update files
|
v
Create Commit
|
v
Create Feature Branch
|
v
Make Independent Changes
|
v
Create Feature Commit
|
v
Find Merge Base
|
v
Three-Way Merge
|
v
Merge Commit
Historical commits remain readable even after later changes because committed manifests and objects are immutable.
Commits references immutable manifests instead of the mutable working tree. This keeps historical snapshots stable.
SHA-256 content addressing allows identical file contents to be stored once and referenced multiple times.
The ObjectStorage interface separates object-management logic from the physical storage backend, allowing local filesystem and S3 implementations.
PostgreSQL stores relationships and metadata, while object storage handles file bytes.
Conditional branch HEAD updates prevent concurrent commits from silently replacing each other's history.
Merge commits preserve both sides of a merge in the commit graph, enabling future history traversal and garbage-collection reachability analysis.
Objects are retained when reachable from any branch's commit history. A grace period and working-tree protection reduce the risk of deleting objects that are still needed.
VersionVault intentionally focuses on the core backend mechanics rather than attempting to reproduce the entire Git feature set.
Current limitations include:
- Local/S3 object storage rather than a distributed object-storage service
- No remote repository synchronization
- No merge-conflict resolution workflow; conflicts are detected and returned
- Object storage and PostgreSQL updates are not globally atomic
- Concurrent working-tree modifications use the application's current consistency model rather than full file-level locking
- No Kubernetes or multi-instance deployment configuration