System Overview The File Storage Service is a robust, scalable solution for handling file uploads, storage, and retrieval operations. Built on Google Cloud Storage (GCS) and PostgreSQL, it provides enterprise-grade file management capabilities with comprehensive SDK support for seamless integration.
Architecture Goals
Scalability : Handle millions of files with horizontal scaling
Reliability : 99.9% uptime with fault tolerance
Security : End-to-end encryption and access control
Performance : Sub-second response times for file operations
Cost Efficiency : Intelligent storage tiering and lifecycle management
High-Level Architecture
graph TB
subgraph "Client Layer"
A[Web Client]
B[Mobile Client]
C[Service Integration]
end
subgraph "API Gateway"
D[Load Balancer]
E[Authentication]
F[Rate Limiting]
end
subgraph "File Storage Service"
G[Upload Controller]
H[Download Controller]
I[Metadata Service]
J[Chunk Manager]
end
subgraph "Storage Layer"
K[PostgreSQL<br/>Metadata DB]
L[Redis<br/>Cache Layer]
M[Google Cloud Storage<br/>File Storage]
end
subgraph "External Services"
N[Notification Service]
O[Audit Service]
P[Monitoring]
end
A --> D
B --> D
C --> D
D --> E
E --> F
F --> G
F --> H
G --> I
H --> I
G --> J
I --> K
I --> L
J --> M
G --> M
H --> M
I --> N
I --> O
G --> P
H --> P
Core Components Deep Dive File Upload Service The upload service handles both single-shot uploads and chunked uploads for large files, implementing resumable upload patterns for reliability.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 @RestController @RequestMapping("/api/v1/files") public class FileUploadController { private final FileUploadService fileUploadService; private final ChunkManagerService chunkManagerService; private final FileValidationService validationService; @PostMapping("/upload") public ResponseEntity<FileUploadResponse> uploadFile ( @RequestParam("file") MultipartFile file, @RequestParam(value = "projectId", required = false) String projectId, @RequestParam(value = "metadata", required = false) String metadata) { validationService.validateFile(file); if (file.getSize() > MAX_SINGLE_UPLOAD_SIZE) { return initiateChunkedUpload(file, projectId, metadata); } else { return uploadDirectly(file, projectId, metadata); } } @PostMapping("/upload/chunked/initiate") public ResponseEntity<ChunkedUploadResponse> initiateChunkedUpload ( @RequestBody ChunkedUploadRequest request) { String uploadId = UUID.randomUUID().toString(); ChunkedUploadSession session = chunkManagerService.createUploadSession( uploadId, request.getFileName(), request.getTotalSize(), request.getChunkSize(), request.getProjectId() ); return ResponseEntity.ok(ChunkedUploadResponse.builder() .uploadId(uploadId) .totalChunks(session.getTotalChunks()) .chunkSize(session.getChunkSize()) .build()); } @PutMapping("/upload/chunked/{uploadId}/chunk/{chunkNumber}") public ResponseEntity<ChunkUploadResponse> uploadChunk ( @PathVariable String uploadId, @PathVariable int chunkNumber, @RequestParam("chunk") MultipartFile chunk) { ChunkUploadResult result = chunkManagerService.uploadChunk( uploadId, chunkNumber, chunk ); if (result.isComplete()) { FileMetadata fileMetadata = chunkManagerService.finalizeUpload(uploadId); return ResponseEntity.ok(ChunkUploadResponse.builder() .chunkNumber(chunkNumber) .completed(true ) .fileUrl(fileMetadata.getDownloadUrl()) .fileId(fileMetadata.getFileId()) .build()); } return ResponseEntity.ok(ChunkUploadResponse.builder() .chunkNumber(chunkNumber) .completed(false ) .build()); } }
Chunk Management System For files larger than 10MB, the system implements intelligent chunking with resumable uploads:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 @Service public class ChunkManagerService { private final RedisTemplate<String, Object> redisTemplate; private final GcsService gcsService; private final FileMetadataRepository metadataRepository; private static final int DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024 ; private static final String CHUNK_SESSION_PREFIX = "chunk_session:" ; public ChunkedUploadSession createUploadSession (String uploadId, String fileName, long totalSize, int chunkSize, String projectId) { int totalChunks = (int ) Math.ceil((double ) totalSize / chunkSize); ChunkedUploadSession session = ChunkedUploadSession.builder() .uploadId(uploadId) .fileName(fileName) .totalSize(totalSize) .chunkSize(chunkSize) .totalChunks(totalChunks) .projectId(projectId) .uploadedChunks(new BitSet (totalChunks)) .createdAt(Instant.now()) .expiresAt(Instant.now().plus(Duration.ofHours(24 ))) .build(); redisTemplate.opsForValue().set( CHUNK_SESSION_PREFIX + uploadId, session, Duration.ofHours(24 ) ); return session; } @Transactional public ChunkUploadResult uploadChunk (String uploadId, int chunkNumber, MultipartFile chunk) { ChunkedUploadSession session = getUploadSession(uploadId); if (session == null ) { throw new UploadSessionNotFoundException ("Upload session not found: " + uploadId); } validateChunk(session, chunkNumber, chunk); String tempChunkPath = generateTempChunkPath(uploadId, chunkNumber); gcsService.uploadChunk(chunk.getInputStream(), tempChunkPath); session.getUploadedChunks().set(chunkNumber - 1 ); updateUploadSession(session); boolean isComplete = session.getUploadedChunks().cardinality() == session.getTotalChunks(); return ChunkUploadResult.builder() .chunkNumber(chunkNumber) .complete(isComplete) .build(); } @Transactional public FileMetadata finalizeUpload (String uploadId) { ChunkedUploadSession session = getUploadSession(uploadId); String finalFilePath = generateFinalFilePath(session); List<String> chunkPaths = generateChunkPaths(uploadId, session.getTotalChunks()); gcsService.combineChunks(chunkPaths, finalFilePath); FileMetadata metadata = FileMetadata.builder() .fileId(UUID.randomUUID().toString()) .originalFileName(session.getFileName()) .gcsPath(finalFilePath) .fileSize(session.getTotalSize()) .projectId(session.getProjectId()) .uploadedAt(Instant.now()) .downloadUrl(gcsService.generateSignedUrl(finalFilePath, Duration.ofDays(365 ))) .build(); metadataRepository.save(metadata); cleanupChunks(chunkPaths); deleteUploadSession(uploadId); return metadata; } }
Database Schema Design The PostgreSQL schema is optimized for query performance and supports file versioning:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 CREATE TABLE files ( file_id VARCHAR (36 ) PRIMARY KEY , original_filename VARCHAR (255 ) NOT NULL , content_type VARCHAR (100 ), file_size BIGINT NOT NULL , gcs_bucket VARCHAR (100 ) NOT NULL , gcs_path VARCHAR (500 ) NOT NULL , project_id VARCHAR (36 ), upload_session_id VARCHAR (36 ), checksum_md5 VARCHAR (32 ), checksum_sha256 VARCHAR (64 ), encryption_key_id VARCHAR (36 ), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), deleted_at TIMESTAMP WITH TIME ZONE, created_by VARCHAR (36 ), tags JSONB, metadata JSONB ); CREATE TABLE file_versions ( version_id VARCHAR (36 ) PRIMARY KEY , file_id VARCHAR (36 ) REFERENCES files(file_id), version_number INTEGER NOT NULL , gcs_path VARCHAR (500 ) NOT NULL , file_size BIGINT NOT NULL , checksum_md5 VARCHAR (32 ), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), created_by VARCHAR (36 ), change_description TEXT ); CREATE TABLE upload_sessions ( session_id VARCHAR (36 ) PRIMARY KEY , original_filename VARCHAR (255 ) NOT NULL , total_size BIGINT NOT NULL , chunk_size INTEGER NOT NULL , total_chunks INTEGER NOT NULL , uploaded_chunks_bitmap TEXT, project_id VARCHAR (36 ), status VARCHAR (20 ) DEFAULT 'IN_PROGRESS' , created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), expires_at TIMESTAMP WITH TIME ZONE NOT NULL , created_by VARCHAR (36 ) ); CREATE INDEX idx_files_project_id ON files(project_id);CREATE INDEX idx_files_created_at ON files(created_at);CREATE INDEX idx_files_deleted_at ON files(deleted_at) WHERE deleted_at IS NULL ;CREATE INDEX idx_files_tags ON files USING GIN(tags);CREATE INDEX idx_file_versions_file_id ON file_versions(file_id);CREATE INDEX idx_upload_sessions_expires_at ON upload_sessions(expires_at);CREATE TABLE files_2024 PARTITION OF files FOR VALUES FROM ('2024-01-01' ) TO ('2025-01-01' );
GCS Integration Service The GCS service handles actual file storage with intelligent lifecycle management:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 @Service public class GcsService { private final Storage storage; private final String bucketName; private final String cdnDomain; private static final Duration DEFAULT_SIGNED_URL_DURATION = Duration.ofHours(1 ); public String uploadFile (InputStream inputStream, String fileName, String contentType, Map<String, String> metadata) { String gcsPath = generateGcsPath(fileName); BlobId blobId = BlobId.of(bucketName, gcsPath); BlobInfo.Builder blobInfoBuilder = BlobInfo.newBuilder(blobId) .setContentType(contentType) .setCacheControl("public, max-age=31536000" ); if (metadata != null ) { blobInfoBuilder.setMetadata(metadata); } StorageClass storageClass = determineStorageClass(fileName, metadata); blobInfoBuilder.setStorageClass(storageClass); BlobInfo blobInfo = blobInfoBuilder.build(); try { Blob blob = storage.create(blobInfo, inputStream); return blob.getName(); } catch (Exception e) { throw new FileStorageException ("Failed to upload file to GCS" , e); } } public String generateSignedUrl (String gcsPath, Duration duration) { BlobId blobId = BlobId.of(bucketName, gcsPath); URL signedUrl = storage.signUrl( BlobInfo.newBuilder(blobId).build(), duration.toMillis(), TimeUnit.MILLISECONDS, Storage.SignUrlOption.httpMethod(HttpMethod.GET) ); return signedUrl.toString(); } public void combineChunks (List<String> chunkPaths, String finalPath) { BlobId finalBlobId = BlobId.of(bucketName, finalPath); Storage.ComposeRequest.Builder composeBuilder = Storage.ComposeRequest.newBuilder() .setTarget(BlobInfo.newBuilder(finalBlobId).build()); for (String chunkPath : chunkPaths) { BlobId chunkBlobId = BlobId.of(bucketName, chunkPath); composeBuilder.addSource(chunkBlobId); } storage.compose(composeBuilder.build()); } private StorageClass determineStorageClass (String fileName, Map<String, String> metadata) { String fileExtension = getFileExtension(fileName); if (isArchiveFile(fileExtension)) { return StorageClass.COLDLINE; } return StorageClass.STANDARD; } @Scheduled(cron = "0 2 * * *") public void optimizeStorageClasses () { } }
File Storage SDK Design The SDK provides a clean, intuitive interface for client applications:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 public class FileStorageClient { private final String baseUrl; private final String apiKey; private final OkHttpClient httpClient; private final ObjectMapper objectMapper; public FileStorageClient (String baseUrl, String apiKey) { this .baseUrl = baseUrl; this .apiKey = apiKey; this .httpClient = createHttpClient(); this .objectMapper = new ObjectMapper (); } public FileUploadResult uploadFile (File file, UploadOptions options) { if (file.length() > options.getChunkThreshold()) { return uploadFileChunked(file, options); } else { return uploadFileDirect(file, options); } } public FileUploadResult uploadFile (File file, UploadOptions options, ProgressCallback progressCallback) { return uploadFileChunked(file, options, progressCallback); } public InputStream downloadFile (String fileId) throws IOException { FileMetadata metadata = getFileMetadata(fileId); return downloadFileByUrl(metadata.getDownloadUrl()); } public FileMetadata getFileMetadata (String fileId) throws IOException { Request request = new Request .Builder() .url(baseUrl + "/api/v1/files/" + fileId + "/metadata" ) .addHeader("Authorization" , "Bearer " + apiKey) .build(); try (Response response = httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { throw new IOException ("Failed to get file metadata: " + response.code()); } return objectMapper.readValue(response.body().string(), FileMetadata.class); } } public PagedResult<FileMetadata> listFiles (FileQuery query) throws IOException { HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl + "/api/v1/files" ).newBuilder(); if (query.getProjectId() != null ) { urlBuilder.addQueryParameter("projectId" , query.getProjectId()); } if (query.getFileType() != null ) { urlBuilder.addQueryParameter("fileType" , query.getFileType()); } urlBuilder.addQueryParameter("page" , String.valueOf(query.getPage())); urlBuilder.addQueryParameter("size" , String.valueOf(query.getSize())); Request request = new Request .Builder() .url(urlBuilder.build()) .addHeader("Authorization" , "Bearer " + apiKey) .build(); try (Response response = httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { throw new IOException ("Failed to list files: " + response.code()); } TypeReference<PagedResult<FileMetadata>> typeRef = new TypeReference <PagedResult<FileMetadata>>() {}; return objectMapper.readValue(response.body().string(), typeRef); } } public void deleteFile (String fileId) throws IOException { Request request = new Request .Builder() .url(baseUrl + "/api/v1/files/" + fileId) .delete() .addHeader("Authorization" , "Bearer " + apiKey) .build(); try (Response response = httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { throw new IOException ("Failed to delete file: " + response.code()); } } } private FileUploadResult uploadFileChunked (File file, UploadOptions options, ProgressCallback progressCallback) { try { ChunkedUploadResponse initResponse = initiateChunkedUpload(file, options); long uploadedBytes = 0 ; int chunkSize = initResponse.getChunkSize(); try (FileInputStream fis = new FileInputStream (file)) { for (int chunkNumber = 1 ; chunkNumber <= initResponse.getTotalChunks(); chunkNumber++) { byte [] buffer = new byte [Math.min(chunkSize, (int ) (file.length() - uploadedBytes))]; int bytesRead = fis.read(buffer); if (bytesRead > 0 ) { uploadChunk(initResponse.getUploadId(), chunkNumber, Arrays.copyOf(buffer, bytesRead)); uploadedBytes += bytesRead; if (progressCallback != null ) { progressCallback.onProgress(uploadedBytes, file.length()); } } } } return FileUploadResult.builder() .fileId(initResponse.getFinalFileId()) .downloadUrl(initResponse.getFinalDownloadUrl()) .build(); } catch (Exception e) { throw new FileUploadException ("Failed to upload file" , e); } } }
Security Implementation Authentication and Authorization 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Component public class FileAccessSecurityService { private final JwtTokenProvider tokenProvider; private final UserService userService; private final ProjectService projectService; public boolean canAccessFile (String userId, String fileId, FileOperation operation) { FileMetadata fileMetadata = getFileMetadata(fileId); User user = userService.getUser(userId); if (fileMetadata.getProjectId() != null ) { ProjectPermission permission = projectService.getUserPermission( userId, fileMetadata.getProjectId() ); return hasRequiredPermission(permission, operation); } return fileMetadata.getCreatedBy().equals(userId) || user.isAdmin(); } @PreAuthorize("@fileAccessSecurityService.canAccessFile(authentication.name, #fileId, 'READ')") public FileMetadata getFile (String fileId) { return fileMetadataRepository.findById(fileId) .orElseThrow(() -> new FileNotFoundException ("File not found: " + fileId)); } private boolean hasRequiredPermission (ProjectPermission permission, FileOperation operation) { switch (operation) { case READ: return permission.canRead(); case WRITE: return permission.canWrite(); case DELETE: return permission.canDelete(); default : return false ; } } }
File Encryption 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 @Service public class FileEncryptionService { private final GoogleKmsService kmsService; private final String keyRingName; public EncryptedUploadStream encryptFile (InputStream inputStream, String fileId) { try { byte [] dek = generateDataEncryptionKey(); String encryptedDek = kmsService.encrypt(keyRingName, dek); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding" ); SecretKeySpec keySpec = new SecretKeySpec (dek, "AES" ); cipher.init(Cipher.ENCRYPT_MODE, keySpec); FileEncryptionMetadata encryptionMetadata = FileEncryptionMetadata.builder() .fileId(fileId) .encryptedDek(encryptedDek) .algorithm("AES-256-GCM" ) .keyVersion(kmsService.getCurrentKeyVersion()) .build(); return new EncryptedUploadStream ( new CipherInputStream (inputStream, cipher), encryptionMetadata ); } catch (Exception e) { throw new EncryptionException ("Failed to encrypt file" , e); } } public InputStream decryptFile (InputStream encryptedStream, String fileId) { try { FileEncryptionMetadata metadata = getEncryptionMetadata(fileId); byte [] dek = kmsService.decrypt(metadata.getEncryptedDek()); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding" ); SecretKeySpec keySpec = new SecretKeySpec (dek, "AES" ); cipher.init(Cipher.DECRYPT_MODE, keySpec); return new CipherInputStream (encryptedStream, cipher); } catch (Exception e) { throw new DecryptionException ("Failed to decrypt file" , e); } } }
Caching Layer Implementation 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 @Service public class FileCacheService { private final RedisTemplate<String, Object> redisTemplate; private final GcsService gcsService; private static final String FILE_METADATA_CACHE_PREFIX = "file_metadata:" ; private static final String FILE_CONTENT_CACHE_PREFIX = "file_content:" ; private static final Duration METADATA_TTL = Duration.ofHours(1 ); private static final Duration SMALL_FILE_TTL = Duration.ofMinutes(30 ); @Cacheable(value = "file_metadata", key = "#fileId") public FileMetadata getFileMetadata (String fileId) { String cacheKey = FILE_METADATA_CACHE_PREFIX + fileId; FileMetadata cached = (FileMetadata) redisTemplate.opsForValue().get(cacheKey); if (cached != null ) { return cached; } FileMetadata metadata = fileMetadataRepository.findById(fileId) .orElseThrow(() -> new FileNotFoundException ("File not found: " + fileId)); redisTemplate.opsForValue().set(cacheKey, metadata, METADATA_TTL); return metadata; } public InputStream getFileContent (String fileId) { FileMetadata metadata = getFileMetadata(fileId); if (metadata.getFileSize() < 1024 * 1024 ) { String cacheKey = FILE_CONTENT_CACHE_PREFIX + fileId; byte [] cachedContent = (byte []) redisTemplate.opsForValue().get(cacheKey); if (cachedContent != null ) { return new ByteArrayInputStream (cachedContent); } byte [] content = gcsService.downloadFileAsBytes(metadata.getGcsPath()); redisTemplate.opsForValue().set(cacheKey, content, SMALL_FILE_TTL); return new ByteArrayInputStream (content); } return gcsService.downloadFile(metadata.getGcsPath()); } }
Database Query Optimization 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 @Repository public class FileMetadataRepository { @PersistenceContext private EntityManager entityManager; public Page<FileMetadata> findFilesWithFilters (FileSearchCriteria criteria, Pageable pageable) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<FileMetadata> query = cb.createQuery(FileMetadata.class); Root<FileMetadata> root = query.from(FileMetadata.class); List<Predicate> predicates = new ArrayList <>(); predicates.add(cb.isNull(root.get("deletedAt" ))); if (criteria.getProjectId() != null ) { predicates.add(cb.equal(root.get("projectId" ), criteria.getProjectId())); } if (criteria.getContentType() != null ) { predicates.add(cb.like(root.get("contentType" ), criteria.getContentType() + "%" )); } if (criteria.getMinSize() != null ) { predicates.add(cb.greaterThanOrEqualTo(root.get("fileSize" ), criteria.getMinSize())); } if (criteria.getMaxSize() != null ) { predicates.add(cb.lessThanOrEqualTo(root.get("fileSize" ), criteria.getMaxSize())); } if (criteria.getCreatedAfter() != null ) { predicates.add(cb.greaterThanOrEqualTo(root.get("createdAt" ), criteria.getCreatedAfter())); } if (criteria.getCreatedBefore() != null ) { predicates.add(cb.lessThanOrEqualTo(root.get("createdAt" ), criteria.getCreatedBefore())); } if (criteria.getTags() != null && !criteria.getTags().isEmpty()) { for (String tag : criteria.getTags()) { predicates.add(cb.isTrue( cb.function("jsonb_exists" , Boolean.class, root.get("tags" ), cb.literal(tag)) )); } } query.where(predicates.toArray(new Predicate [0 ])); query.orderBy(cb.desc(root.get("createdAt" ))); TypedQuery<FileMetadata> typedQuery = entityManager.createQuery(query); typedQuery.setFirstResult((int ) pageable.getOffset()); typedQuery.setMaxResults(pageable.getPageSize()); List<FileMetadata> results = typedQuery.getResultList(); CriteriaQuery<Long> countQuery = cb.createQuery(Long.class); Root<FileMetadata> countRoot = countQuery.from(FileMetadata.class); countQuery.select(cb.count(countRoot)); countQuery.where(predicates.toArray(new Predicate [0 ])); Long totalCount = entityManager.createQuery(countQuery).getSingleResult(); return new PageImpl <>(results, pageable, totalCount); } }
Monitoring and Observability Comprehensive Monitoring Setup 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 @Component public class FileStorageMetrics { private final MeterRegistry meterRegistry; private final Timer uploadTimer; private final Timer downloadTimer; private final Counter uploadSuccessCounter; private final Counter uploadFailureCounter; private final Gauge activeUploadsGauge; public FileStorageMetrics (MeterRegistry meterRegistry) { this .meterRegistry = meterRegistry; this .uploadTimer = Timer.builder("file_upload_duration" ) .description("Time taken to upload files" ) .tag("service" , "file-storage" ) .register(meterRegistry); this .downloadTimer = Timer.builder("file_download_duration" ) .description("Time taken to download files" ) .tag("service" , "file-storage" ) .register(meterRegistry); this .uploadSuccessCounter = Counter.builder("file_upload_success_total" ) .description("Total number of successful file uploads" ) .tag("service" , "file-storage" ) .register(meterRegistry); this .uploadFailureCounter = Counter.builder("file_upload_failure_total" ) .description("Total number of failed file uploads" ) .tag("service" , "file-storage" ) .register(meterRegistry); this .activeUploadsGauge = Gauge.builder("file_upload_active_count" ) .description("Number of currently active uploads" ) .tag("service" , "file-storage" ) .register(meterRegistry, this , FileStorageMetrics::getActiveUploadsCount); } public void recordUploadSuccess (String fileType, long fileSizeBytes, Duration duration) { uploadSuccessCounter.increment( Tags.of( "file_type" , fileType, "size_category" , categorizeFileSize(fileSizeBytes) ) ); uploadTimer.record(duration); DistributionSummary.builder("file_upload_size_bytes" ) .tag("file_type" , fileType) .register(meterRegistry) .record(fileSizeBytes); } public void recordUploadFailure (String fileType, String errorType, Duration duration) { uploadFailureCounter.increment( Tags.of( "file_type" , fileType, "error_type" , errorType ) ); } private String categorizeFileSize (long sizeBytes) { if (sizeBytes < 1024 * 1024 ) return "small" ; if (sizeBytes < 10 * 1024 * 1024 ) return "medium" ; if (sizeBytes < 100 * 1024 * 1024 ) return "large" ; return "xlarge" ; } private double getActiveUploadsCount () { return uploadSessionService.getActiveUploadCount(); } } @Component public class FileStorageHealthIndicator implements HealthIndicator { private final GcsService gcsService; private final DataSource dataSource; private final RedisTemplate<String, Object> redisTemplate; @Override public Health health () { Health.Builder healthBuilder = Health.up(); try (Connection connection = dataSource.getConnection()) { if (!connection.isValid(5 )) { return Health.down() .withDetail("database" , "Connection validation failed" ) .build(); } healthBuilder.withDetail("database" , "UP" ); } catch (Exception e) { return Health.down() .withDetail("database" , "Connection failed: " + e.getMessage()) .build(); } try { redisTemplate.execute((RedisCallback<String>) connection -> { connection.ping(); return "PONG" ; }); healthBuilder.withDetail("redis" , "UP" ); } catch (Exception e) { healthBuilder.withDetail("redis" , "DOWN: " + e.getMessage()); } try { boolean gcsHealthy = gcsService.checkHealth(); healthBuilder.withDetail("gcs" , gcsHealthy ? "UP" : "DOWN" ); if (!gcsHealthy) { return healthBuilder.down().build(); } } catch (Exception e) { return Health.down() .withDetail("gcs" , "Health check failed: " + e.getMessage()) .build(); } return healthBuilder.build(); } }
Error Handling and Resilience Comprehensive Error Handling Strategy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 @ControllerAdvice public class FileStorageExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(FileStorageExceptionHandler.class); @ExceptionHandler(FileNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public ErrorResponse handleFileNotFound (FileNotFoundException e) { logger.warn("File not found: {}" , e.getMessage()); return ErrorResponse.builder() .code("FILE_NOT_FOUND" ) .message("The requested file was not found" ) .timestamp(Instant.now()) .build(); } @ExceptionHandler(FileSizeExceededException.class) @ResponseStatus(HttpStatus.PAYLOAD_TOO_LARGE) public ErrorResponse handleFileSizeExceeded (FileSizeExceededException e) { logger.warn("File size exceeded: {}" , e.getMessage()); return ErrorResponse.builder() .code("FILE_SIZE_EXCEEDED" ) .message("File size exceeds the maximum allowed limit" ) .details(Map.of("maxSize" , e.getMaxAllowedSize(), "actualSize" , e.getActualSize())) .timestamp(Instant.now()) .build(); } @ExceptionHandler(UnsupportedFileTypeException.class) @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE) public ErrorResponse handleUnsupportedFileType (UnsupportedFileTypeException e) { logger.warn("Unsupported file type: {}" , e.getMessage()); return ErrorResponse.builder() .code("UNSUPPORTED_FILE_TYPE" ) .message("The file type is not supported" ) .details(Map.of("supportedTypes" , e.getSupportedTypes())) .timestamp(Instant.now()) .build(); } @ExceptionHandler(StorageException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponse handleStorageException (StorageException e) { logger.error("Storage operation failed" , e); return ErrorResponse.builder() .code("STORAGE_ERROR" ) .message("An error occurred while processing the file" ) .timestamp(Instant.now()) .build(); } @ExceptionHandler(RateLimitExceededException.class) @ResponseStatus(HttpStatus.TOO_MANY_REQUESTS) public ErrorResponse handleRateLimit (RateLimitExceededException e) { logger.warn("Rate limit exceeded for user: {}" , e.getUserId()); return ErrorResponse.builder() .code("RATE_LIMIT_EXCEEDED" ) .message("Upload rate limit exceeded" ) .details(Map.of("retryAfter" , e.getRetryAfterSeconds())) .timestamp(Instant.now()) .build(); } } @Component public class ResilientGcsService { private final Storage storage; private final RetryTemplate retryTemplate; private final CircuitBreaker circuitBreaker; public ResilientGcsService (Storage storage) { this .storage = storage; this .retryTemplate = createRetryTemplate(); this .circuitBreaker = createCircuitBreaker(); } @Retryable(value = {StorageException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2)) public String uploadWithRetry (InputStream inputStream, String path, String contentType) { return circuitBreaker.executeSupplier(() -> retryTemplate.execute(context -> { logger.info("Attempting upload, attempt: {}" , context.getRetryCount() + 1 ); return doUpload(inputStream, path, contentType); }) ); } private RetryTemplate createRetryTemplate () { RetryTemplate template = new RetryTemplate (); FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy (); backOffPolicy.setBackOffPeriod(2000 ); template.setBackOffPolicy(backOffPolicy); SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy (); retryPolicy.setMaxAttempts(3 ); template.setRetryPolicy(retryPolicy); return template; } private CircuitBreaker createCircuitBreaker () { return CircuitBreaker.ofDefaults("gcs-upload" ); } }
Production Deployment Architecture Kubernetes Deployment Configuration 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 apiVersion: apps/v1 kind: Deployment metadata: name: file-storage-service labels: app: file-storage-service spec: replicas: 3 selector: matchLabels: app: file-storage-service template: metadata: labels: app: file-storage-service spec: containers: - name: file-storage-service image: file-storage-service:latest ports: - containerPort: 8080 env: - name: SPRING_PROFILES_ACTIVE value: "production" - name: DATABASE_URL valueFrom: secretKeyRef: name: file-storage-secrets key: database-url - name: REDIS_URL valueFrom: secretKeyRef: name: file-storage-secrets key: redis-url - name: GCS_BUCKET_NAME valueFrom: configMapKeyRef: name: file-storage-config key: gcs-bucket-name resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "1Gi" cpu: "500m" livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 30 periodSeconds: 30 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 5 periodSeconds: 5 volumeMounts: - name: gcs-key mountPath: "/etc/gcs" readOnly: true volumes: - name: gcs-key secret: secretName: gcs-service-account-key --- apiVersion: v1 kind: Service metadata: name: file-storage-service spec: selector: app: file-storage-service ports: - port: 80 targetPort: 8080 type: ClusterIP --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: file-storage-ingress annotations: kubernetes.io/ingress.class: "nginx" cert-manager.io/cluster-issuer: "letsencrypt-prod" nginx.ingress.kubernetes.io/proxy-body-size: "100m" spec: tls: - hosts: - api.fileservice.com secretName: file-storage-tls rules: - host: api.fileservice.com http: paths: - path: / pathType: Prefix backend: service: name: file-storage-service port: number: 80
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 # GCS Bucket Configuration resource "google_storage_bucket" "file_storage_bucket" { name = "company-file-storage-${var.environment}" location = "US" force_destroy = false versioning { enabled = true } lifecycle_rule { condition { age = 30 } action { type = "SetStorageClass" storage_class = "NEARLINE" } } lifecycle_rule { condition { age = 90 } action { type = "SetStorageClass" storage_class = "COLDLINE" } } lifecycle_rule { condition { age = 365 } action { type = "SetStorageClass" storage_class = "ARCHIVE" } } cors { origin = ["https://app.company.com"] method = ["GET", "HEAD", "PUT", "POST", "DELETE"] response_header = ["*"] max_age_seconds = 3600 } } # Cloud SQL PostgreSQL Instance resource "google_sql_database_instance" "file_storage_db" { name = "file-storage-db-${var.environment}" database_version = "POSTGRES_13" region = "us-central1" settings { tier = "db-custom-2-4096" backup_configuration { enabled = true start_time = "03:00" location = "us" point_in_time_recovery_enabled = true backup_retention_settings { retained_backups = 30 } } ip_configuration { ipv4_enabled = false private_network = google_compute_network.vpc.id } database_flags { name = "max_connections" value = "200" } database_flags { name = "shared_preload_libraries" value = "pg_stat_statements" } } } # Redis Instance for Caching resource "google_redis_instance" "file_storage_cache" { name = "file-storage-cache-${var.environment}" tier = "STANDARD_HA" memory_size_gb = 4 region = "us-central1" redis_version = "REDIS_6_X" display_name = "File Storage Cache" authorized_network = google_compute_network.vpc.id }
Service Level Objectives 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 slo_targets: availability: target: 99.9 % measurement_window: 30_days latency: upload_p95: 5s upload_p99: 15s download_p95: 500ms download_p99: 2s throughput: max_concurrent_uploads: 1000 max_upload_rate: 10GB/s max_download_rate: 50GB/s error_rate: target: 0.1 % measurement_window: 24_hours
Load Testing Strategy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 @Component public class FileStorageLoadTester { private final FileStorageClient client; private final ExecutorService executorService; public LoadTestResult runLoadTest (LoadTestConfig config) { List<CompletableFuture<UploadResult>> futures = new ArrayList <>(); for (int i = 0 ; i < config.getConcurrentUsers(); i++) { CompletableFuture<UploadResult> future = CompletableFuture.supplyAsync(() -> { return simulateUserUpload(config); }, executorService); futures.add(future); } List<UploadResult> results = futures.stream() .map(CompletableFuture::join) .collect(Collectors.toList()); return analyzeResults(results); } private UploadResult simulateUserUpload (LoadTestConfig config) { long startTime = System.currentTimeMillis(); try { byte [] testData = generateTestFile(config.getFileSize()); FileUploadResult result = client.uploadFile( new ByteArrayInputStream (testData), "test-file-" + UUID.randomUUID().toString(), "application/octet-stream" ); long duration = System.currentTimeMillis() - startTime; return UploadResult.builder() .success(true ) .duration(duration) .fileSize(testData.length) .fileId(result.getFileId()) .build(); } catch (Exception e) { long duration = System.currentTimeMillis() - startTime; return UploadResult.builder() .success(false ) .duration(duration) .error(e.getMessage()) .build(); } } }
Interview Questions and Insights System Design Questions Q: How would you handle a scenario where files need to be replicated across multiple regions for disaster recovery?
A: I would implement a multi-region replication strategy:
Primary-Secondary Pattern : Use GCS multi-region buckets for automatic replication
Database Replication : Set up PostgreSQL read replicas in different regions
Metadata Consistency : Implement eventual consistency with conflict resolution
Failover Logic : Automatic failover with health checks and circuit breakers
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Service public class MultiRegionReplicationService { private final Map<String, GcsService> regionalGcsServices; private final CircuitBreaker circuitBreaker; public FileUploadResult uploadWithReplication (MultipartFile file, String primaryRegion) { String primaryPath = uploadToPrimary(file, primaryRegion); CompletableFuture.runAsync(() -> replicateToSecondaryRegions(file, primaryPath)); return buildUploadResult(primaryPath); } private void replicateToSecondaryRegions (MultipartFile file, String primaryPath) { regionalGcsServices.entrySet().parallelStream() .filter(entry -> !entry.getKey().equals(primaryRegion)) .forEach(entry -> { try { circuitBreaker.executeSupplier(() -> entry.getValue().replicateFile(primaryPath, file) ); } catch (Exception e) { logger.error("Failed to replicate to region: {}" , entry.getKey(), e); } }); } }
Q: How would you optimize the system for handling millions of small files vs. thousands of large files?
A: Different optimization strategies are needed:
For Small Files (< 1MB):
Batch multiple small files into larger objects
Use aggressive caching in Redis
Implement file bundling/archiving
Use CDN for frequently accessed files
For Large Files (> 100MB):
Mandatory chunked uploads with resumability
Implement progressive download with range requests
Use streaming processing to avoid memory issues
Implement intelligent storage class selection
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Service public class FileOptimizationService { public StorageStrategy determineOptimalStrategy (FileMetadata file) { if (file.getFileSize() < SMALL_FILE_THRESHOLD) { return StorageStrategy.builder() .cacheInRedis(true ) .bundleWithOthers(shouldBundle(file)) .storageClass(StorageClass.STANDARD) .cdnEnabled(true ) .build(); } else if (file.getFileSize() > LARGE_FILE_THRESHOLD) { return StorageStrategy.builder() .chunkedUpload(true ) .resumableUpload(true ) .storageClass(determineStorageClass(file)) .compressionEnabled(shouldCompress(file)) .build(); } return StorageStrategy.defaultStrategy(); } }
Q: How would you implement file deduplication to save storage costs?
A: Implement content-based deduplication using cryptographic hashing:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 @Service public class FileDeduplicationService { private final FileHashRepository hashRepository; private final GcsService gcsService; @Transactional public FileUploadResult uploadWithDeduplication (MultipartFile file, String projectId) { String sha256Hash = calculateSHA256(file); String md5Hash = calculateMD5(file); Optional<FileMetadata> existingFile = hashRepository.findByHash(sha256Hash); if (existingFile.isPresent()) { return createFileReference(existingFile.get(), projectId); } String gcsPath = gcsService.uploadFile(file.getInputStream(), generateFileName(), file.getContentType()); FileMetadata metadata = FileMetadata.builder() .fileId(UUID.randomUUID().toString()) .originalFileName(file.getOriginalFilename()) .gcsPath(gcsPath) .fileSize(file.getSize()) .projectId(projectId) .sha256Hash(sha256Hash) .md5Hash(md5Hash) .build(); return fileMetadataRepository.save(metadata); } private FileUploadResult createFileReference (FileMetadata originalFile, String projectId) { FileMetadata referenceMetadata = originalFile.toBuilder() .fileId(UUID.randomUUID().toString()) .projectId(projectId) .createdAt(Instant.now()) .isReference(true ) .originalFileId(originalFile.getFileId()) .build(); fileMetadataRepository.save(referenceMetadata); return FileUploadResult.builder() .fileId(referenceMetadata.getFileId()) .downloadUrl(generateDownloadUrl(referenceMetadata)) .deduplicated(true ) .originalFileId(originalFile.getFileId()) .build(); } }
Scalability Questions Q: How would you handle rate limiting to prevent abuse while maintaining good user experience?
A: Implement a multi-tier rate limiting strategy:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 @Component public class FileUploadRateLimiter { private final RedisTemplate<String, String> redisTemplate; private final Map<UserTier, RateLimitConfig> tierLimits = Map.of( UserTier.FREE, new RateLimitConfig (10 , Duration.ofMinutes(1 ), 100 * 1024 * 1024L ), UserTier.PREMIUM, new RateLimitConfig (100 , Duration.ofMinutes(1 ), 10 * 1024 * 1024 * 1024L ), UserTier.ENTERPRISE, new RateLimitConfig (1000 , Duration.ofMinutes(1 ), Long.MAX_VALUE) ); public boolean allowUpload (String userId, long fileSize) { UserTier userTier = getUserTier(userId); RateLimitConfig config = tierLimits.get(userTier); String rateLimitKey = "rate_limit:" + userId; if (!checkRateLimit(rateLimitKey, config.getRequestsPerWindow(), config.getTimeWindow())) { throw new RateLimitExceededException ("Request rate limit exceeded" ); } String bandwidthKey = "bandwidth:" + userId + ":" + LocalDate.now(); long currentUsage = getCurrentBandwidthUsage(bandwidthKey); if (currentUsage + fileSize > config.getDailyBandwidthLimit()) { throw new BandwidthLimitExceededException ("Daily bandwidth limit exceeded" ); } redisTemplate.opsForValue().increment(bandwidthKey, fileSize); redisTemplate.expire(bandwidthKey, Duration.ofDays(1 )); return true ; } private boolean checkRateLimit (String key, int limit, Duration window) { String script = """ local key = KEYS[1] local limit = tonumber(ARGV[1]) local window = tonumber(ARGV[2]) local now = tonumber(ARGV[3]) redis.call('zremrangebyscore', key, 0, now - window) local current = redis.call('zcard', key) if current < limit then redis.call('zadd', key, now, now) redis.call('expire', key, window) return 1 else return 0 end """ ; Long result = redisTemplate.execute( RedisScript.of(script, Long.class), List.of(key), String.valueOf(limit), String.valueOf(window.toMillis()), String.valueOf(System.currentTimeMillis()) ); return result != null && result == 1 ; } }
SDK Usage Examples Java SDK Usage 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 public class FileStorageExamples { public static void main (String[] args) { FileStorageClient client = new FileStorageClient ( "https://api.fileservice.com" , "your-api-key" ); File file = new File ("document.pdf" ); UploadOptions options = UploadOptions.builder() .projectId("project-123" ) .tags(Set.of("document" , "pdf" )) .metadata(Map.of("department" , "engineering" )) .build(); try { FileUploadResult result = client.uploadFile(file, options); System.out.println("File uploaded successfully: " + result.getFileId()); System.out.println("Download URL: " + result.getDownloadUrl()); } catch (FileUploadException e) { System.err.println("Upload failed: " + e.getMessage()); } File largeFile = new File ("large-video.mp4" ); FileUploadResult result = client.uploadFile(largeFile, options, (uploaded, total) -> { double progress = (double ) uploaded / total * 100 ; System.out.printf("Upload progress: %.2f%%\n" , progress); }); try (InputStream inputStream = client.downloadFile(result.getFileId())) { Files.copy(inputStream, Paths.get("downloaded-file.mp4" )); System.out.println("File downloaded successfully" ); } catch (IOException e) { System.err.println("Download failed: " + e.getMessage()); } FileQuery query = FileQuery.builder() .projectId("project-123" ) .fileType("image/*" ) .createdAfter(LocalDateTime.now().minusDays(7 )) .tags(Set.of("processed" )) .page(0 ) .size(20 ) .build(); PagedResult<FileMetadata> files = client.listFiles(query); files.getContent().forEach(file -> System.out.println("File: " + file.getOriginalFileName() + " - " + file.getFileSize() + " bytes" ) ); } }
React SDK Usage 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import { FileStorageClient } from '@company/file-storage-sdk' ;const App = ( ) => { const [client] = useState (() => new FileStorageClient ({ baseUrl : 'https://api.fileservice.com' , apiKey : process.env .REACT_APP_API_KEY })); const [uploadProgress, setUploadProgress] = useState (0 ); const handleFileUpload = async (file ) => { try { const result = await client.uploadFile (file, { projectId : 'project-123' , onProgress : (loaded, total ) => { setUploadProgress ((loaded / total) * 100 ); } }); console .log ('Upload successful:' , result); } catch (error) { console .error ('Upload failed:' , error); } }; const handleFileDrop = (acceptedFiles ) => { acceptedFiles.forEach (file => { handleFileUpload (file); }); }; return ( <div > <FileDropzone onDrop ={handleFileDrop} /> {uploadProgress > 0 && ( <ProgressBar value ={uploadProgress} /> )} </div > ); };
Security Best Practices Content Validation and Sanitization 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 @Service public class FileSecurityService { private final Set<String> allowedContentTypes = Set.of( "image/jpeg" , "image/png" , "image/gif" , "image/webp" , "application/pdf" , "text/plain" , "application/json" , "video/mp4" , "video/webm" ); private final Set<String> dangerousExtensions = Set.of( ".exe" , ".bat" , ".cmd" , ".scr" , ".pif" , ".com" , ".vbs" , ".js" ); public void validateFile (MultipartFile file) { if (file.getSize() > MAX_FILE_SIZE) { throw new FileSizeExceededException ("File size exceeds maximum allowed size" ); } String contentType = file.getContentType(); if (contentType == null || !allowedContentTypes.contains(contentType.toLowerCase())) { throw new UnsupportedFileTypeException ("File type not allowed: " + contentType); } String filename = file.getOriginalFilename(); if (filename != null && hasDangerousExtension(filename)) { throw new SecurityException ("File extension not allowed" ); } scanForMalware(file); validateFileSignature(file, contentType); } private void scanForMalware (MultipartFile file) { try { AntivirusResult result = antivirusService.scanFile(file.getBytes()); if (!result.isClean()) { throw new SecurityException ("File contains malware: " + result.getThreatName()); } } catch (Exception e) { throw new SecurityException ("Unable to scan file for malware" , e); } } private void validateFileSignature (MultipartFile file, String declaredContentType) { try { byte [] header = new byte [12 ]; file.getInputStream().read(header); String detectedType = detectContentType(header); if (!declaredContentType.equals(detectedType)) { throw new SecurityException ("File signature doesn't match declared content type" ); } } catch (IOException e) { throw new SecurityException ("Unable to validate file signature" , e); } } }
Access Control Implementation 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 @Service public class FileAccessControlService { private final JwtTokenProvider tokenProvider; private final PermissionEvaluator permissionEvaluator; @PreAuthorize("@fileAccessControlService.canAccessFile(authentication, #fileId, 'READ')") public FileMetadata getFileMetadata (String fileId) { return fileMetadataRepository.findById(fileId) .orElseThrow(() -> new FileNotFoundException ("File not found: " + fileId)); } public boolean canAccessFile (Authentication authentication, String fileId, String operation) { String userId = authentication.getName(); FileMetadata file = getFileMetadata(fileId); if (file.getCreatedBy().equals(userId)) { return true ; } if (file.getProjectId() != null ) { return permissionEvaluator.hasPermission(authentication, file.getProjectId(), "Project" , operation); } if (file.getVisibility() == FileVisibility.PUBLIC && "READ" .equals(operation)) { return true ; } return hasExplicitFilePermission(userId, fileId, operation); } private boolean hasExplicitFilePermission (String userId, String fileId, String operation) { return filePermissionRepository.existsByFileIdAndUserIdAndPermission( fileId, userId, FilePermission.valueOf(operation) ); } }
Disaster Recovery and Backup Strategy Automated Backup System 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 @Component public class FileBackupService { private final GcsService primaryGcsService; private final GcsService backupGcsService; private final FileMetadataRepository fileMetadataRepository; @Scheduled(cron = "0 2 * * *") public void performIncrementalBackup () { LocalDateTime lastBackup = getLastBackupTime(); LocalDateTime now = LocalDateTime.now(); List<FileMetadata> modifiedFiles = fileMetadataRepository .findByUpdatedAtBetween(lastBackup, now); BackupResult result = BackupResult.builder() .startTime(now) .totalFiles(modifiedFiles.size()) .build(); try { List<CompletableFuture<FileBackupResult>> futures = modifiedFiles.stream() .map(file -> CompletableFuture.supplyAsync(() -> backupFile(file))) .collect(Collectors.toList()); List<FileBackupResult> backupResults = futures.stream() .map(CompletableFuture::join) .collect(Collectors.toList()); result = result.toBuilder() .endTime(LocalDateTime.now()) .successCount(backupResults.stream().mapToInt(r -> r.isSuccess() ? 1 : 0 ).sum()) .failureCount(backupResults.stream().mapToInt(r -> r.isSuccess() ? 0 : 1 ).sum()) .build(); recordBackupCompletion(result); } catch (Exception e) { logger.error("Backup process failed" , e); alertService.sendBackupFailureAlert(e); } } private FileBackupResult backupFile (FileMetadata fileMetadata) { try { String backupPath = generateBackupPath(fileMetadata); primaryGcsService.copyFile(fileMetadata.getGcsPath(), backupGcsService, backupPath); FileBackupMetadata backupMetadata = FileBackupMetadata.builder() .originalFileId(fileMetadata.getFileId()) .backupPath(backupPath) .backupTime(LocalDateTime.now()) .backupSize(fileMetadata.getFileSize()) .checksum(fileMetadata.getChecksumSha256()) .build(); fileBackupRepository.save(backupMetadata); return FileBackupResult.success(fileMetadata.getFileId()); } catch (Exception e) { logger.error("Failed to backup file: {}" , fileMetadata.getFileId(), e); return FileBackupResult.failure(fileMetadata.getFileId(), e.getMessage()); } } @Scheduled(cron = "0 3 * * 0") public void performFullBackup () { logger.info("Starting full backup process" ); exportDatabase(); performFullFileBackup(); verifyBackupIntegrity(); } public RestoreResult restoreFromBackup (RestoreRequest request) { try { if (request.getRestoreType() == RestoreType.POINT_IN_TIME) { return restoreToPointInTime(request.getTargetTime()); } else if (request.getRestoreType() == RestoreType.SPECIFIC_FILES) { return restoreSpecificFiles(request.getFileIds()); } else { return restoreFullSystem(request.getBackupId()); } } catch (Exception e) { logger.error("Restore operation failed" , e); return RestoreResult.failure(e.getMessage()); } } }
Multi-Region Deployment Flow
graph TB
subgraph "Primary Region (US-Central)"
A[API Gateway] --> B[File Service Instances]
B --> C[Primary PostgreSQL]
B --> D[Primary GCS Bucket]
B --> E[Redis Cache]
end
subgraph "Secondary Region (Europe-West)"
F[API Gateway] --> G[File Service Instances]
G --> H[Read Replica PostgreSQL]
G --> I[Secondary GCS Bucket]
G --> J[Redis Cache]
end
subgraph "Disaster Recovery Region (Asia-Southeast)"
K[Standby API Gateway] --> L[Standby File Service]
L --> M[Backup PostgreSQL]
L --> N[Archive GCS Bucket]
end
C --> H
D --> I
D --> N
O[Global Load Balancer] --> A
O --> F
O --> K
P[Health Checker] --> O
Q[Failover Controller] --> O
style A fill:#e1f5fe
style F fill:#e8f5e8
style K fill:#fff3e0
Cost Optimization Strategies Intelligent Storage Class Management 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 @Service public class StorageCostOptimizer { private final GcsService gcsService; private final FileMetadataRepository fileMetadataRepository; private final FileAccessLogRepository accessLogRepository; @Scheduled(cron = "0 1 * * *") public void optimizeStorageClasses () { LocalDateTime cutoffDate = LocalDateTime.now().minusDays(30 ); List<FileMetadata> candidates = fileMetadataRepository.findInfrequentlyAccessedFiles(cutoffDate); for (FileMetadata file : candidates) { StorageOptimizationDecision decision = analyzeFile(file); if (decision.shouldChangeStorageClass()) { try { gcsService.changeStorageClass(file.getGcsPath(), decision.getTargetStorageClass()); file.setStorageClass(decision.getTargetStorageClass()); file.setLastOptimized(LocalDateTime.now()); fileMetadataRepository.save(file); recordCostSavings(file, decision); } catch (Exception e) { logger.error("Failed to optimize storage for file: {}" , file.getFileId(), e); } } } } private StorageOptimizationDecision analyzeFile (FileMetadata file) { long accessCount = accessLogRepository.countByFileIdAndAccessTimeAfter( file.getFileId(), LocalDateTime.now().minusDays(30 ) ); LocalDateTime lastAccess = accessLogRepository.findLastAccessTime(file.getFileId()); long daysSinceLastAccess = ChronoUnit.DAYS.between(lastAccess, LocalDateTime.now()); if (daysSinceLastAccess > 365 ) { return StorageOptimizationDecision.moveToArchive(); } else if (daysSinceLastAccess > 90 && accessCount < 5 ) { return StorageOptimizationDecision.moveToColdline(); } else if (daysSinceLastAccess > 30 && accessCount < 10 ) { return StorageOptimizationDecision.moveToNearline(); } return StorageOptimizationDecision.noChange(); } private void recordCostSavings (FileMetadata file, StorageOptimizationDecision decision) { BigDecimal originalCost = calculateMonthlyCost(file.getFileSize(), file.getCurrentStorageClass()); BigDecimal newCost = calculateMonthlyCost(file.getFileSize(), decision.getTargetStorageClass()); BigDecimal savings = originalCost.subtract(newCost); CostOptimizationRecord record = CostOptimizationRecord.builder() .fileId(file.getFileId()) .optimizationDate(LocalDateTime.now()) .fromStorageClass(file.getCurrentStorageClass()) .toStorageClass(decision.getTargetStorageClass()) .fileSize(file.getFileSize()) .monthlySavings(savings) .build(); costOptimizationRepository.save(record); costSavingsCounter.increment(Tags.of("storage_class" , decision.getTargetStorageClass().name()), savings.doubleValue()); } }
Usage Analytics and Reporting 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 @Service public class FileStorageAnalyticsService { public StorageAnalyticsReport generateMonthlyReport (String projectId, YearMonth month) { LocalDateTime startOfMonth = month.atDay(1 ).atStartOfDay(); LocalDateTime endOfMonth = month.atEndOfMonth().atTime(23 , 59 , 59 ); StorageUsageMetrics usage = StorageUsageMetrics.builder() .totalFiles(fileMetadataRepository.countByProjectIdAndCreatedAtBetween( projectId, startOfMonth, endOfMonth)) .totalStorageBytes(fileMetadataRepository.sumFileSizeByProjectIdAndDateRange( projectId, startOfMonth, endOfMonth)) .averageFileSize(calculateAverageFileSize(projectId, startOfMonth, endOfMonth)) .build(); AccessPatternMetrics access = AccessPatternMetrics.builder() .totalDownloads(accessLogRepository.countDownloadsByProjectAndDateRange( projectId, startOfMonth, endOfMonth)) .totalUploads(uploadLogRepository.countUploadsByProjectAndDateRange( projectId, startOfMonth, endOfMonth)) .uniqueUsers(accessLogRepository.countUniqueUsersByProjectAndDateRange( projectId, startOfMonth, endOfMonth)) .build(); CostAnalysisMetrics cost = CostAnalysisMetrics.builder() .storageCost(calculateStorageCost(usage)) .bandwidthCost(calculateBandwidthCost(access)) .operationsCost(calculateOperationsCost(access)) .totalCost(calculateTotalCost(usage, access)) .projectedCost(projectCostForNextMonth(usage, access)) .build(); List<FileUsageStats> topFilesBySize = getTopFilesBySize(projectId, 10 ); List<FileUsageStats> topFilesByAccess = getTopFilesByAccess(projectId, 10 ); return StorageAnalyticsReport.builder() .projectId(projectId) .reportMonth(month) .generatedAt(LocalDateTime.now()) .storageUsage(usage) .accessPatterns(access) .costAnalysis(cost) .topFilesBySize(topFilesBySize) .topFilesByAccess(topFilesByAccess) .recommendations(generateRecommendations(usage, access, cost)) .build(); } private List<CostOptimizationRecommendation> generateRecommendations ( StorageUsageMetrics usage, AccessPatternMetrics access, CostAnalysisMetrics cost) { List<CostOptimizationRecommendation> recommendations = new ArrayList <>(); if (cost.getStorageCost().compareTo(cost.getBandwidthCost()) > 0 ) { recommendations.add(CostOptimizationRecommendation.builder() .type(RecommendationType.STORAGE_CLASS_OPTIMIZATION) .description("Consider moving infrequently accessed files to cheaper storage classes" ) .potentialSavings(estimateStorageClassSavings(usage)) .priority(RecommendationPriority.HIGH) .build()); } long oldFilesCount = fileMetadataRepository.countOldFiles( usage.getProjectId(), LocalDateTime.now().minusYears(1 ) ); if (oldFilesCount > 1000 ) { recommendations.add(CostOptimizationRecommendation.builder() .type(RecommendationType.LIFECYCLE_POLICY) .description("Implement automatic archival for files older than 1 year" ) .potentialSavings(estimateLifecycleSavings(oldFilesCount)) .priority(RecommendationPriority.MEDIUM) .build()); } return recommendations; } }
Testing Strategy Integration Testing 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @TestPropertySource(properties = { "spring.datasource.url=jdbc:h2:mem:testdb", "gcs.bucket-name=test-bucket", "redis.host=localhost" }) class FileStorageIntegrationTest { @Autowired private TestRestTemplate restTemplate; @Autowired private FileMetadataRepository fileMetadataRepository; @MockBean private GcsService gcsService; @Test void shouldUploadFileSuccessfully () throws Exception { MockMultipartFile file = new MockMultipartFile ( "file" , "test.txt" , "text/plain" , "Hello World" .getBytes() ); when (gcsService.uploadFile(any(), any(), any(), any())) .thenReturn("test-bucket/files/test.txt" ); when (gcsService.generateSignedUrl(any(), any())) .thenReturn("https://storage.googleapis.com/test-bucket/files/test.txt" ); MultiValueMap<String, Object> body = new LinkedMultiValueMap <>(); body.add("file" , file.getResource()); body.add("projectId" , "test-project" ); HttpHeaders headers = new HttpHeaders (); headers.setContentType(MediaType.MULTIPART_FORM_DATA); headers.setBearerAuth("test-token" ); ResponseEntity<FileUploadResponse> response = restTemplate.exchange( "/api/v1/files/upload" , HttpMethod.POST, new HttpEntity <>(body, headers), FileUploadResponse.class ); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody().getFileId()).isNotNull(); assertThat(response.getBody().getDownloadUrl()).contains("storage.googleapis.com" ); Optional<FileMetadata> savedFile = fileMetadataRepository.findById(response.getBody().getFileId()); assertThat(savedFile).isPresent(); assertThat(savedFile.get().getOriginalFileName()).isEqualTo("test.txt" ); } @Test void shouldHandleChunkedUploadCorrectly () throws Exception { String uploadId = UUID.randomUUID().toString(); int totalChunks = 3 ; int chunkSize = 1024 ; ChunkedUploadRequest initRequest = ChunkedUploadRequest.builder() .fileName("large-file.mp4" ) .totalSize(3072 ) .chunkSize(chunkSize) .projectId("test-project" ) .build(); ResponseEntity<ChunkedUploadResponse> initResponse = restTemplate.postForEntity( "/api/v1/files/upload/chunked/initiate" , initRequest, ChunkedUploadResponse.class ); assertThat(initResponse.getStatusCode()).isEqualTo(HttpStatus.OK); String actualUploadId = initResponse.getBody().getUploadId(); for (int i = 1 ; i <= totalChunks; i++) { MockMultipartFile chunk = new MockMultipartFile ( "chunk" , "chunk" + i, "application/octet-stream" , new byte [i == totalChunks ? 1024 : chunkSize] ); MultiValueMap<String, Object> chunkBody = new LinkedMultiValueMap <>(); chunkBody.add("chunk" , chunk.getResource()); ResponseEntity<ChunkUploadResponse> chunkResponse = restTemplate.exchange( "/api/v1/files/upload/chunked/" + actualUploadId + "/chunk/" + i, HttpMethod.PUT, new HttpEntity <>(chunkBody, createAuthHeaders()), ChunkUploadResponse.class ); assertThat(chunkResponse.getStatusCode()).isEqualTo(HttpStatus.OK); if (i == totalChunks) { assertThat(chunkResponse.getBody().isCompleted()).isTrue(); assertThat(chunkResponse.getBody().getFileId()).isNotNull(); } } } @Test void shouldEnforceRateLimitsCorrectly () { String userId = "test-user" ; for (int i = 0 ; i < 15 ; i++) { MockMultipartFile file = new MockMultipartFile ( "file" , "test" + i + ".txt" , "text/plain" , ("Content " + i).getBytes() ); MultiValueMap<String, Object> body = new LinkedMultiValueMap <>(); body.add("file" , file.getResource()); ResponseEntity<String> response = restTemplate.exchange( "/api/v1/files/upload" , HttpMethod.POST, new HttpEntity <>(body, createAuthHeaders(userId)), String.class ); if (i < 10 ) { assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } else { assertThat(response.getStatusCode()).isEqualTo(HttpStatus.TOO_MANY_REQUESTS); } } } }
Load Testing with JMeter Configuration 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 <?xml version="1.0" encoding="UTF-8" ?> <jmeterTestPlan version ="1.2" > <hashTree > <TestPlan > <stringProp name ="TestPlan.comments" > File Storage Service Load Test</stringProp > <boolProp name ="TestPlan.functional_mode" > false</boolProp > <boolProp name ="TestPlan.tearDown_on_shutdown" > true</boolProp > <boolProp name ="TestPlan.serialize_threadgroups" > false</boolProp > <elementProp name ="TestPlan.arguments" elementType ="Arguments" guiclass ="ArgumentsPanel" > <collectionProp name ="Arguments.arguments" > <elementProp name ="base_url" elementType ="Argument" > <stringProp name ="Argument.name" > base_url</stringProp > <stringProp name ="Argument.value" > https://api.fileservice.com</stringProp > </elementProp > </collectionProp > </elementProp > <stringProp name ="TestPlan.user_define_classpath" > </stringProp > </TestPlan > <hashTree > <ThreadGroup guiclass ="ThreadGroupGui" testclass ="ThreadGroup" testname ="File Upload Load Test" > <stringProp name ="ThreadGroup.on_sample_error" > continue</stringProp > <elementProp name ="ThreadGroup.main_controller" elementType ="LoopController" > <boolProp name ="LoopController.continue_forever" > false</boolProp > <stringProp name ="LoopController.loops" > 10</stringProp > </elementProp > <stringProp name ="ThreadGroup.num_threads" > 100</stringProp > <stringProp name ="ThreadGroup.ramp_time" > 60</stringProp > <boolProp name ="ThreadGroup.scheduler" > false</boolProp > <stringProp name ="ThreadGroup.duration" > </stringProp > <stringProp name ="ThreadGroup.delay" > </stringProp > </ThreadGroup > <hashTree > <HTTPSamplerProxy > <elementProp name ="HTTPsampler.Files" elementType ="HTTPFileArgs" > <collectionProp name ="HTTPFileArgs.files" > <elementProp name ="" elementType ="HTTPFileArg" > <stringProp name ="File.path" > ${__P(test_file_path)}</stringProp > <stringProp name ="File.paramname" > file</stringProp > <stringProp name ="File.mimetype" > application/octet-stream</stringProp > </elementProp > </collectionProp > </elementProp > <elementProp name ="HTTPsampler.Arguments" elementType ="Arguments" > <collectionProp name ="Arguments.arguments" > <elementProp name ="projectId" elementType ="HTTPArgument" > <boolProp name ="HTTPArgument.always_encode" > false</boolProp > <stringProp name ="Argument.value" > load-test-project</stringProp > <stringProp name ="Argument.name" > projectId</stringProp > </elementProp > </collectionProp > </elementProp > <stringProp name ="HTTPSampler.domain" > ${base_url}</stringProp > <stringProp name ="HTTPSampler.port" > </stringProp > <stringProp name ="HTTPSampler.protocol" > https</stringProp > <stringProp name ="HTTPSampler.contentEncoding" > </stringProp > <stringProp name ="HTTPSampler.path" > /api/v1/files/upload</stringProp > <stringProp name ="HTTPSampler.method" > POST</stringProp > <boolProp name ="HTTPSampler.follow_redirects" > true</boolProp > <boolProp name ="HTTPSampler.auto_redirects" > false</boolProp > <boolProp name ="HTTPSampler.use_keepalive" > true</boolProp > <boolProp name ="HTTPSampler.DO_MULTIPART_POST" > true</boolProp > </HTTPSamplerProxy > </hashTree > </hashTree > </hashTree > </jmeterTestPlan >
External Resources and Documentation Essential Resources Google Cloud Storage Documentation:
PostgreSQL Performance:
Spring Boot Integration:
Monitoring and Observability:
Security Best Practices:
This comprehensive file storage service design provides a production-ready, scalable solution that handles millions of files efficiently while maintaining high availability, security, and performance standards. The architecture supports both small and large file operations, implements intelligent cost optimization, and provides robust monitoring and disaster recovery capabilities.