On this page
Object Storage
Micronaut Object Storage provides a uniform API to create, read and delete objects in the major cloud providers:
There is also a local storage implementation for testing purposes.
Using this API enables the creation of truly multi-cloud, portable applications.
Micronaut Object Storage also provides a reactive companion API that mirrors the
blocking contract with Reactive Streams Publisher return types.
For this project, you can find a list of releases (with release notes) here:
To get started, you need to declare a dependency for the actual cloud provider you are using. See the actual cloud provider documentation for more details:
Then, you can inject in your controllers/services/etc. a bean of type ObjectStorageOperations, the parent interface that allows you to use the API in a generic way for all cloud providers:
@Singleton
public class ProfileService {
private static final Logger LOG = LoggerFactory.getLogger(ProfileService.class);
private final ObjectStorageOperations<?, ?, ?> objectStorage;
public ProfileService(ObjectStorageOperations<?, ?, ?> objectStorage) {
this.objectStorage = objectStorage;
}
}If your application is not multi-cloud, and/or you need cloud-specific details, you can use a concrete implementation. For example, for AWS S3:
@Controller
public class UploadController {
private final AwsS3Operations objectStorage;
public UploadController(AwsS3Operations objectStorage) {
this.objectStorage = objectStorage;
}
}If you have multiple object storages configured, it is possible to select which one to work with via bean qualifiers.
For example, given the following configuration:
src/main/resources/application-ec2.ymlmicronaut.object-storage.aws.pictures.bucket=pictures-bucket
micronaut.object-storage.aws.logos.bucket=logos-bucketYou then need to use @Named("pictures") or @Named("logos") to specify which of the object storages you want to use.
Resolving resources by URI
Configured storages also publish Micronaut’s ResourceLoader beans, so you can resolve objects through ResourceResolver instead of injecting an ObjectStorageOperations bean directly.
For the configuration above, the following URI resolves through the pictures storage:
pictures://avatars/logo.pngThis resolution stays additive to the existing API:
-
Existing
@Namedinjection continues to work unchanged. -
Provider-native aliases resolve only when the target bucket or container is already backed by a configured storage.
-
Storage names must not reuse reserved Micronaut prefixes such as
classpath,file,string, orbase64, and they must not collide with the provider aliasess3,gs,azb, oros.
Uploading files
In case you want to have better control of the upload options used, you can use the method
upload(UploadRequest, Consumer) of ObjectStorageOperations, which will give you access to the
cloud vendor-specific request class or builder.
For example, for AWS S3:
UploadResponse<PutObjectResponse> response = objectStorage.upload(objectStorageUpload, builder -> {
builder.acl(ObjectCannedACL.PUBLIC_READ);
});If you receive a StreamingFileUpload in a multipart controller, you can forward it to object storage without first converting it to a local file or byte array:
@Post(uri = "/stream", consumes = MediaType.MULTIPART_FORM_DATA, produces = MediaType.TEXT_PLAIN)
public HttpResponse<String> streamingUpload(StreamingFileUpload fileUpload) {
UploadRequest objectStorageUpload = UploadRequest.fromStreamingFileUpload(fileUpload, "uploads/" + fileUpload.getFilename());
UploadResponse<PutObjectResponse> response = objectStorage.upload(objectStorageUpload);
return HttpResponse
.created(response.getKey())
.header("ETag", response.getNativeResponse().eTag());
}Retrieving files
If you prefer URI-based resolution, you can retrieve the same object through ResourceResolver:
ResourceResolver resourceResolver = new ResourceResolver(resourceLoaders);
Optional<InputStream> logo = resourceResolver.getResourceAsStream("pictures://avatars/logo.png");Deleting files
See the dedicated Pre-Signed Uploads section for portable, time-limited client upload requests.
See the dedicated Paginated Listing section for provider-agnostic page-by-page object listing.
Micronaut Object Storage provides ReactiveObjectStorageOperations as an additive, Reactive Streams-based companion to ObjectStorageOperations.
The reactive API mirrors the blocking contract:
-
uploads emit UploadResponse
-
retrieve emits
Optional<ObjectStorageEntry<?>> -
delete, exists, list, and paginated listing each emit a single result
-
copy returns a completion-only
Publisher<Void>
Example injection:
package example;
import io.micronaut.objectstorage.ReactiveObjectStorageOperations;
import jakarta.inject.Singleton;
@Singleton
class ReactiveProfileService {
private final ReactiveObjectStorageOperations<?, ?, ?> objectStorage;
ReactiveProfileService(ReactiveObjectStorageOperations<?, ?, ?> objectStorage) {
this.objectStorage = objectStorage;
}
}Phase 1 keeps the existing request and entry abstractions unchanged:
-
UploadRequest still provides an
InputStream -
ObjectStorageEntry still exposes an
InputStream
That means the operation lifecycle is reactive, while payload handling remains stream-based. Fully reactive payload streaming would require new additive request and entry abstractions in a follow-up change.
Reactive beans are created alongside the existing blocking beans, so current injections of ObjectStorageOperations continue to work unchanged. To opt in, inject ReactiveObjectStorageOperations instead.
Provider behavior in phase 1 is intentionally split:
-
AWS S3 uses the AWS SDK v2
S3AsyncClient -
Azure Blob Storage uses Azure async blob clients
-
Oracle Cloud Infrastructure uses the OCI
ObjectStorageAsyncClient -
Google Cloud Storage and local storage keep the same additive reactive API, but currently adapt their existing blocking implementations onto Micronaut’s blocking executor
Pre-Signed Uploads
Applications can ask supporting providers to create a time-limited HTTP request for uploading a single object without proxying the file bytes through the application.
Use CreatePresignedUploadRequest to describe the object key and signing requirements, then
call createPresignedUpload(…) on ObjectStorageOperations.
The returned PresignedUpload includes:
-
the target URI
-
the HTTP method to use
-
the headers the caller must forward exactly
-
the expiration instant
Portable contract:
-
The signed request uploads exactly one object key.
-
Clients must send an HTTP
PUT. -
Clients must preserve all required headers from the response.
-
Provider support is optional. Providers that do not support pre-signed uploads, or cannot sign in the current configuration, return
Optional.empty(). -
Oracle Cloud uses OCI pre-authenticated requests for this API and returns the upload URL as the portable signed request URI.
-
Local storage does not support this API because it has no HTTP endpoint to sign.
The following controller example returns a JSON payload your frontend can use to upload directly to object storage:
Paginated Listing
The paginated listing API exposes ObjectStorageOperations#listObjects(ListObjectsRequest) which returns a ListObjectsResponse containing the ordered keys for the current page and an opaque continuation token for the next page. The request accepts a raw prefix for simple starts-with filtering and normalizes empty strings to absent. The examples below show how to iterate pages by replaying the continuation token returned by each response. Providers do not promise global ordering across all keys, so the safe pattern is: use a small deterministic page size, filter by a raw prefix such as userId + "/", and repeat requests while replaying the continuation token until the response omits it.
|
Note
|
the examples intentionally keep the code provider-agnostic and rely only on ObjectStorageOperations API. |
Bucket and Container Management
The BucketOperations API adds provider-agnostic lifecycle management for object storage buckets and containers
without changing the existing ObjectStorageOperations contract.
@Singleton
public class BucketService {
private final BucketOperations<?> bucketOperations;
private final ReactiveBucketOperations<?> reactiveBucketOperations;
public BucketService(@Named("default") BucketOperations<?> bucketOperations,
@Named("default") ReactiveBucketOperations<?> reactiveBucketOperations) {
this.bucketOperations = bucketOperations;
this.reactiveBucketOperations = reactiveBucketOperations;
}
}Use the injected bean to create, retrieve, and delete provider-managed buckets or containers by name:
public void manageBucket(String name) {
bucketOperations.create(name);
boolean exists = bucketOperations.exists(name);
bucketOperations.retrieve(name).ifPresent(bucket -> {
System.out.println(bucket.name());
});
if (exists) {
bucketOperations.delete(name);
}
}The provider-specific native metadata is exposed through BucketEntry#nativeEntry() for advanced use cases, while
the portable API keeps the logical bucket or container name available via BucketEntry#name().
If you also need a non-blocking lifecycle API, inject ReactiveBucketOperations and subscribe to the returned
`Publisher`s the same way you would with the reactive object-storage contract:
public void manageBucketReactive(String name) {
Publisher<Void> create = reactiveBucketOperations.create(name);
Publisher<Boolean> exists = reactiveBucketOperations.exists(name);
}|
Note
|
Creating or deleting a bucket or container does not retarget an already injected ObjectStorageOperations bean.
Existing object-operation beans continue to use the configured default bucket or container for their named storage.
|
Provider notes:
-
AWS and Oracle Cloud use native async SDK clients for
ReactiveBucketOperations. -
Google Cloud Storage uses bucket terminology, but the
google-cloud-storageclient on3.0.xdoes not expose a native async bucket API, soReactiveBucketOperationsbridges the blocking client on the Micronaut blocking executor for that provider. -
Azure Blob Storage uses container terminology, but the same
BucketOperationsAPI is exposed for portability. -
Azure Blob Storage uses the SDK’s async container client for
ReactiveBucketOperations. -
Local storage maps bucket names to sibling directories under the configured local storage root.
-
Oracle Cloud bucket lifecycle operations require
micronaut.object-storage.oracle-cloud.<name>.compartment-idin addition to the existingbucketandnamespaceconfiguration.
To use Amazon S3, you need the following dependency:
implementation("io.micronaut.objectstorage:micronaut-object-storage-aws")Refer to the Micronaut AWS documentation for more information about credentials and region configuration.
The object storage specific configuration options available are:
For example:
src/main/resources/application-ec2.ymlmicronaut.object-storage.aws.default.bucket=profile-pictures-bucketThe concrete implementation of ObjectStorageOperations is AwsS3Operations.
You can also resolve objects through Micronaut’s ResourceLoader abstraction:
-
Named storage URI:
pictures://avatars/logo.png -
AWS alias URI:
s3://pictures-bucket/avatars/logo.png
The s3: alias only resolves buckets that are already configured under micronaut.object-storage.aws.
Advanced configuration
For configuration properties other than the specified above, you can add bean to your application that implements
BeanCreatedEventListener. For example:
@Singleton
public class S3ClientBuilderCustomizer implements BeanCreatedEventListener<S3ClientBuilder> {
@Override
public S3ClientBuilder onCreated(@NonNull BeanCreatedEvent<S3ClientBuilder> event) {
return event.getBean()
.overrideConfiguration(c -> c.apiCallTimeout(Duration.of(60, ChronoUnit.SECONDS)));
}
}|
Tip
|
See the guide for Use the Micronaut Object Storage API to Store Files in Amazon S3 to learn more. |
To use Azure Blob Storage, you need the following dependency:
implementation("io.micronaut.objectstorage:micronaut-object-storage-azure")Refer to the Micronaut Azure documentation for more information about authentication options.
The object storage specific configuration options available are:
For example:
src/main/resources/application-azure.ymlazure.credential.client-secret.client-id=<client-id>
azure.credential.client-secret.tenant-id=<tenant-id>
azure.credential.client-secret.secret=<secret>
micronaut.object-storage.azure.default.container=profile-pictures-container
micronaut.object-storage.azure.default.endpoint=https://my-account.blob.core.windows.netThe concrete implementation of ObjectStorageOperations is AzureBlobStorageOperations.
You can also resolve objects through Micronaut’s ResourceLoader abstraction:
-
Named storage URI:
pictures://avatars/logo.png -
Azure alias URI:
azb:storageaccount://pictures/avatars/logo.png
The azb: alias only resolves configured storages, matching the account name from endpoint together with the
configured container name.
Advanced configuration
For configuration properties other than the specified above, you can add bean to your application that implements
BeanCreatedEventListener. For example:
@Singleton
public class BlobServiceClientBuilderCustomizer implements BeanCreatedEventListener<BlobServiceClientBuilder> {
@Override
public BlobServiceClientBuilder onCreated(@NonNull BeanCreatedEvent<BlobServiceClientBuilder> event) {
HttpPipelinePolicy noOp = (context, next) -> next.process();
return event.getBean().addPolicy(noOp);
}
}To use Google Cloud Storage, you need the following dependency:
implementation("io.micronaut.objectstorage:micronaut-object-storage-gcp")Refer to the Micronaut GCP documentation for more information about configuring your GCP project.
The object storage specific configuration options available are:
For example:
src/main/resources/application-gcp.ymlgcp.project-id=my-gcp-project
micronaut.object-storage.gcp.default.bucket=profile-pictures-bucketThe concrete implementation of ObjectStorageOperations is GoogleCloudStorageOperations.
You can also resolve objects through Micronaut’s ResourceLoader abstraction:
-
Named storage URI:
pictures://avatars/logo.png -
Google Cloud alias URI:
gs://pictures-bucket/avatars/logo.png
The gs: alias only resolves buckets that are already configured under micronaut.object-storage.gcp.
Advanced configuration
For configuration properties other than the specified above, you can add bean to your application that implements
BeanCreatedEventListener. For example:
@Singleton
public class StorageOptionsBuilderCustomizer implements BeanCreatedEventListener<StorageOptions.Builder> {
@Override
public StorageOptions.Builder onCreated(@NonNull BeanCreatedEvent<StorageOptions.Builder> event) {
return event.getBean()
.setTransportOptions(HttpTransportOptions.newBuilder().setConnectTimeout(60_000).build());
}
}|
Tip
|
See the guide for Use the Micronaut Object Storage API to Store Files in Google Cloud Storage to learn more. |
To use Oracle Cloud Infrastructure (OCI) Object Storage, you need the following dependency:
implementation("io.micronaut.objectstorage:micronaut-object-storage-oracle-cloud")Refer to the Micronaut Oracle Cloud documentation for more information about authentication options.
The object storage specific configuration options available are:
For example:
src/main/resources/application-oraclecloud.ymloci.config.profile=DEFAULT
micronaut.object-storage.oracle-cloud.default.bucket=profile-pictures-bucket
micronaut.object-storage.oracle-cloud.default.namespace=MyNamespaceThe concrete implementation of ObjectStorageOperations is OracleCloudStorageOperations
You can also resolve objects through Micronaut’s ResourceLoader abstraction:
-
Named storage URI:
pictures://avatars/logo.png -
Oracle Cloud alias URI:
os:us-ashburn-1:my-namespace://pictures-bucket/avatars/logo.png
The os: alias only resolves configured storages and must match the active OCI region together with the configured
namespace and bucket.
Advanced configuration
For configuration properties other than the specified above, you can add bean to your application that implements
BeanCreatedEventListener. For example:
//See https://github.com/oracle/oci-java-sdk/blob/master/bmc-examples/src/main/java/ClientConfigurationTimeoutExample.java
@Singleton
public class ObjectStorageClientBuilderCustomizer implements BeanCreatedEventListener<ObjectStorageClient.Builder> {
public static final int CONNECTION_TIMEOUT_IN_MILLISECONDS = 25000;
public static final int READ_TIMEOUT_IN_MILLISECONDS = 35000;
@Override
public ObjectStorageClient.Builder onCreated(@NonNull BeanCreatedEvent<ObjectStorageClient.Builder> event) {
ClientConfiguration clientConfiguration =
ClientConfiguration.builder()
.connectionTimeoutMillis(CONNECTION_TIMEOUT_IN_MILLISECONDS)
.readTimeoutMillis(READ_TIMEOUT_IN_MILLISECONDS)
.build();
return event.getBean()
.configuration(clientConfiguration);
}
}|
Tip
|
See the guide for Use the Micronaut Object Storage API to Store Files in Oracle Cloud Infrastructure (OCI) Object Storage to learn more. |
To use the local storage implementation (useful for tests), you need the following dependency:
testImplementation("io.micronaut.objectstorage:micronaut-object-storage-local")Then, simply define a local storage:
micronaut.object-storage.local.default.enabled=true|
Note
|
When added to the classpath, LocalStorageOperations becomes the primary implementation of ObjectStorageOperations. |
|
Note
|
The local storage implementation reserves the .metadata key namespace for per-object metadata files. User object
keys cannot be .metadata or start with .metadata/ (or .metadata\ on platforms where the file separator is \).
|
By default, it will create a temporary folder to store the files, but you can configure it to use a specific folder:
On POSIX-capable file systems, configured bucket paths created by the local implementation use owner-only permissions for
bucket subdirectories, stored objects, and .metadata files. On non-POSIX file systems, Micronaut cannot apply those
permissions directly, so custom storage paths on shared hosts should be provisioned with restrictive ACLs by the caller.
For example:
src/main/resources/application-test.ymlmicronaut.object-storage.local.default.path=/tmp/my-object-storageThe concrete implementation of ObjectStorageOperations is LocalStorageOperations.
The Micronaut Control Panel module has support for Micronaut Object Storage by adding the following dependency:
developmentOnly("io.micronaut.controlpanel:micronaut-control-panel-object-storage")Check the documentation for more information the documentation for more information
See the following list of guides to learn more about working with Object Storage in the Micronaut Framework:
This section documents breaking changes between Micronaut Object Storage versions:
Micronaut Object Storage 3.0.0
Deprecations
-
The constructor
io.micronaut.objectstorage.azure.AzureBlobStorageEntry(String, BinaryData)deprecated previously has been removed. UseAzureBlobStorageEntry(String, BinaryData, BlobProperties)instead. -
The bean constructor
io.micronaut.objectstorage.oraclecloud.OracleCloudStorageOperations(OracleCloudStorageConfiguration, ObjectStorage)deprecated previously has been removed.OracleCloudStorageOperations(OracleCloudStorageConfiguration, ObjectStorage, RegionProvider)is used instead.
You can find the source code of this project in this repository: