Databases: Comparison, Choosing & Polyglot
jsonb: Indexes, Tradeoffs & Exercise
You know how to store text and numbers in PostgreSQL. But what about files, profile pictures, uploaded CVs, invoices, or exported reports?
Storing files in a database is possible but almost always the wrong choice. Another solution can be storing files on the server’s storage (SSD or Hard drive). But this can be very tricky in a world of Docker containers and distributed applications - your application may run on multiple servers in parallel. A different type of storage was built specifically for files: object storage.
Object storage is a system designed specifically for storing files - photos, PDFs, videos, zip files. Think of it as a giant filing cabinet in the cloud where you put a file in, give it a name, and retrieve it later by that name.
You access everything through a simple HTTP API - upload with PUT, download with GET. No SQL, no tables.

Object storage has no real folders. avatars/user-42.jpg is just a string with a slash in it -there is no actual folder called avatars. This simplicity is what lets it scale to billions of files.
| Object Storage | Database (PostgreSQL) | Filesystem | |
|---|---|---|---|
| Best for | Large binary files | Structured data | Local temp files |
| Access | HTTP API | SQL | File path |
| Scales to | Billions of files | Millions of rows | One machine |
| Cost per GB | Very low | Higher | Local disk |
| Example | Profile photo (5MB) | User record | Config file |
Store the file in object storage. Store the reference (key or URL) in the database.
Object storage is the right tool whenever you have large binary files that do not belong in a database:
| Use case | Example |
|---|---|
| User profile pictures | Avatar uploaded when signing up |
| Documents | CV uploaded to a job board |
| Invoices and reports | Generated PDF sent to a customer |
| Backups | Database dump stored off-server |
| Large media | Video recordings, podcast audio |
These three concepts are the building blocks of every object storage system.

Bucket - the top-level container. Like a drive or a top-level folder. Every file belongs to exactly one bucket. Bucket names must be unique.
Object - one stored file. It has three parts: the raw bytes, a key, and metadata.
Key - the unique name of the object inside the bucket. It is just a string. Slashes make it look like a folder path but there are no real folders underneath. Use a UUID in the key to prevent two users overwriting each other's files: avatars/a3f2b1c4-photo.jpg.
Public bucket - anyone with the URL can access the file directly in their browser. No login needed. Good for logos and public images. Risk: the file is permanently accessible to anyone who knows the URL.
Private bucket - direct URLs return 403 Forbidden. Your Spring Boot app checks if the user is authorised and generates a signed URL. The user uses that URL to download directly from storage. Good for user uploads, invoices, medical records - anything sensitive.
Object storage is accessed through HTTP. The operations map directly to HTTP methods:
| Operation | HTTP | What happens |
|---|---|---|
| Upload | PUT | Store a new file |
| Download | GET | Retrieve a file |
| Delete | DELETE | Remove a file |
| List | GET (bucket) | List all files |
When Amazon built S3 in 2006 they also defined an API standard for how clients talk to object storage — how to upload, download, list, and delete files. That standard became so widely adopted that today almost every object storage provider implements it, including Backblaze B2, Cloudflare R2, MinIO, and DigitalOcean Spaces.
This matters for you as a developer because it means:

In practice you never write raw HTTP PUT and GET requests yourself. You use the AWS SDK which handles authentication, request signing, retries, and error handling for you. The SDK is just a clean Java wrapper around those HTTP calls - but knowing that HTTP is underneath helps you understand what the SDK is actually doing.
A presigned URL is a time-limited URL that gives temporary access to a private file — without needing credentials. After the time expires, the URL stops working automatically.

The key insight: Spring Boot never touches the file bytes. It only handles authorisation and generates the signed URL. The client downloads directly from Backblaze B2 — your server uses no bandwidth for the file transfer itself.
The URL contains an expiry timestamp and a cryptographic signature. After 15 minutes the URL stops working — no action needed on your side, it expires automatically.
Why presigned URLs are better than alternatives:
| Approach | Problem |
|---|---|
| Make bucket public | Anyone can access any file permanently |
| Proxy through Spring Boot | Your server downloads and re-streams — wastes bandwidth and compute |
| Presigned URL | Client downloads directly, URL expires automatically, server only does auth check |
Here is the integration overview first — what the Spring Boot app needs and how it connects:

You need four things to connect Spring Boot to Backblaze B2. Now let's look at each one:
Step 1 — pom.xml: Add the AWS SDK. Backblaze B2 is S3-compatible so the same SDK works.
Step 2 — application.yaml: Add your B2 credentials (endpoint, region, keys, bucket name).
Step 3 — B2Config.java: Creates the S3Client bean — same @Configuration + @Bean pattern as AppConfig.java.
Step 4 — FileService.java: Three methods — upload, get presigned URL, delete.
Now let's see how these fit together in a request:

Backblaze B2 is free (no credit card), S3-compatible, and simple to set up. You just need an email to sign up.
For the full account setup steps see the Backblaze B2 Setup Guide document.
Once you have your account details you will have:
endpoint — e.g. https://s3.us-west-004.backblazeb2.comregion — e.g. us-west-004keyId — your access keyapplicationKey — your secret keybucket — the bucket name you createdBackblaze B2 is S3-compatible so you use the AWS SDK pointed at Backblaze. Add to pom.xml:
xml
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>2.25.0</version>
</dependency>