Google Cloud Run is a fully managed serverless platform that automatically scales your stateless containers. This guide covers building, deploying, and optimizing Java applications for Cloud Run.
Core Concepts
Why Cloud Run for Java?
- Serverless: No infrastructure management
- Auto-scaling: Scales to zero when not in use
- Pay-per-use: Only pay for request processing time
- Container-based: Use any Java framework or library
- Fast deployment: Quick iterations and deployments
Key Considerations for Java on Cloud Run:
- Cold Start Optimization: Minimize startup time
- Memory Management: Configure appropriate memory limits
- Stateless Design: No local storage or session state
- Health Checks: Implement proper readiness/liveness probes
- Concurrency: Handle multiple requests per container
Dependencies and Setup
1. Maven Dependencies
<properties> <spring-boot.version>3.1.0</spring-boot.version> <jib-maven-plugin.version>3.4.0</jib-maven-plugin.version> <google-cloud.version>2.20.0</google-cloud.version> </properties> <dependencies> <!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Spring Boot Actuator for Health Checks --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Spring Boot Configuration Processor --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <version>${spring-boot.version}</version> <optional>true</optional> </dependency> <!-- Google Cloud Libraries --> <dependency> <groupId>com.google.cloud</groupId> <artifactId>google-cloud-storage</artifactId> <version>${google-cloud.version}</version> </dependency> <dependency> <groupId>com.google.cloud</groupId> <artifactId>google-cloud-secretmanager</artifactId> <version>${google-cloud.version}</version> </dependency> <dependency> <groupId>com.google.cloud</groupId> <artifactId>google-cloud-logging</artifactId> <version>${google-cloud.version}</version> </dependency> <!-- Database (PostgreSQL) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.6.0</version> </dependency> <!-- Redis for Caching --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Testing --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>${spring-boot.version}</version> <scope>test</scope> </dependency> <!-- Testcontainers for Integration Tests --> <dependency> <groupId>org.testcontainers</groupId> <artifactId>junit-jupiter</artifactId> <version>1.18.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>postgresql</artifactId> <version>1.18.3</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <!-- Spring Boot Maven Plugin --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring-boot.version}</version> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> <!-- Jib for Container Building --> <plugin> <groupId>com.google.cloud.tools</groupId> <artifactId>jib-maven-plugin</artifactId> <version>${jib-maven-plugin.version}</version> <configuration> <from> <image>eclipse-temurin:21-jre</image> </from> <to> <image>gcr.io/${google.cloud.project}/${project.artifactId}:${project.version}</image> </to> <container> <jvmFlags> <jvmFlag>-Xmx512m</jvmFlag> <jvmFlag>-Xms256m</jvmFlag> <jvmFlag>-XX:+UseG1GC</jvmFlag> <jvmFlag>-XX:MaxRAMPercentage=75.0</jvmFlag> </jvmFlags> <environment> <SPRING_PROFILES_ACTIVE>cloudrun</SPRING_PROFILES_ACTIVE> </environment> <ports> <port>8080</port> </ports> <creationTime>USE_CURRENT_TIMESTAMP</creationTime> </container> </configuration> </plugin> </plugins> </build> 2. Google Cloud SDK Setup
# Install Google Cloud SDK curl https://sdk.cloud.google.com | bash exec -l $SHELL # Initialize and authenticate gcloud init gcloud auth configure-docker # Set project gcloud config set project YOUR_PROJECT_ID
Application Implementation
1. Main Application Class
package com.example.cloudrun; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; @SpringBootApplication @EnableConfigurationProperties(CloudRunProperties.class) public class CloudRunApplication { public static void main(String[] args) { SpringApplication.run(CloudRunApplication.class, args); } } 2. Cloud Run Configuration
package com.example.cloudrun.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; @Configuration @ConfigurationProperties(prefix = "cloudrun") public class CloudRunProperties { private String serviceName; private String revision; private String configuration; private int maxInstances = 100; private int concurrency = 80; private int timeoutSeconds = 300; private String region = "us-central1"; // Getters and Setters public String getServiceName() { return serviceName; } public void setServiceName(String serviceName) { this.serviceName = serviceName; } public String getRevision() { return revision; } public void setRevision(String revision) { this.revision = revision; } public String getConfiguration() { return configuration; } public void setConfiguration(String configuration) { this.configuration = configuration; } public int getMaxInstances() { return maxInstances; } public void setMaxInstances(int maxInstances) { this.maxInstances = maxInstances; } public int getConcurrency() { return concurrency; } public void setConcurrency(int concurrency) { this.concurrency = concurrency; } public int getTimeoutSeconds() { return timeoutSeconds; } public void setTimeoutSeconds(int timeoutSeconds) { this.timeoutSeconds = timeoutSeconds; } public String getRegion() { return region; } public void setRegion(String region) { this.region = region; } } 3. Application Configuration
# application-cloudrun.yml server: port: 8080 servlet: context-path: / compression: enabled: true mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json min-response-size: 1024 spring: application: name: cloudrun-demo datasource: url: ${DATABASE_URL:jdbc:postgresql://localhost:5432/cloudrun} username: ${DATABASE_USERNAME:postgres} password: ${DATABASE_PASSWORD:password} hikari: maximum-pool-size: 5 minimum-idle: 1 connection-timeout: 30000 max-lifetime: 1800000 jpa: hibernate: ddl-auto: validate properties: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect jdbc: lob: non_contextual_creation: true show_sql: false format_sql: false redis: host: ${REDIS_HOST:localhost} port: ${REDIS_PORT:6379} password: ${REDIS_PASSWORD:} lettuce: pool: max-active: 8 max-idle: 8 min-idle: 1 max-wait: -1ms cloud: gcp: project-id: ${GOOGLE_CLOUD_PROJECT} credentials: location: ${GOOGLE_APPLICATION_CREDENTIALS:} management: endpoints: web: exposure: include: health,info,metrics,env base-path: /actuator endpoint: health: show-details: always show-components: always probes: enabled: true metrics: enabled: true cloudrun: service-name: ${K_SERVICE:local} revision: ${K_REVISION:local} configuration: ${K_CONFIGURATION:local} max-instances: ${MAX_INSTANCES:100} concurrency: ${CONCURRENCY:80} timeout-seconds: ${TIMEOUT_SECONDS:300} region: ${REGION:us-central1} logging: level: com.example.cloudrun: INFO org.hibernate.SQL: WARN org.springframework.cloud.gcp: INFO pattern: level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]" # Optimize for Cloud Run spring: lifecycle: timeout-per-shutdown-phase: 30s threads: virtual: enabled: true 4. Health Check Configuration
package com.example.cloudrun.health; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.actuate.health.Status; import org.springframework.stereotype.Component; import javax.sql.DataSource; import java.sql.Connection; import java.sql.Statement; import java.util.Map; @Component public class CloudRunHealthIndicator implements HealthIndicator { private final DataSource dataSource; private final CloudRunMetadata cloudRunMetadata; public CloudRunHealthIndicator(DataSource dataSource, CloudRunMetadata cloudRunMetadata) { this.dataSource = dataSource; this.cloudRunMetadata = cloudRunMetadata; } @Override public Health health() { try { // Check database connectivity boolean dbHealthy = checkDatabaseHealth(); // Build health details Health.Builder healthBuilder = dbHealthy ? Health.up() : Health.down(); healthBuilder .withDetail("service", cloudRunMetadata.getServiceName()) .withDetail("revision", cloudRunMetadata.getRevision()) .withDetail("region", cloudRunMetadata.getRegion()) .withDetail("timestamp", System.currentTimeMillis()) .withDetails(buildHealthDetails(dbHealthy)); return healthBuilder.build(); } catch (Exception e) { return Health.down(e) .withDetail("service", cloudRunMetadata.getServiceName()) .withDetail("error", e.getMessage()) .build(); } } private boolean checkDatabaseHealth() { try (Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement()) { return statement.execute("SELECT 1"); } catch (Exception e) { return false; } } private Map<String, Object> buildHealthDetails(boolean dbHealthy) { return Map.of( "database", dbHealthy ? "CONNECTED" : "DISCONNECTED", "memory_usage", Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(), "available_processors", Runtime.getRuntime().availableProcessors(), "uptime", ManagementFactory.getRuntimeMXBean().getUptime() ); } } @Component class CloudRunMetadata { @Value("${K_SERVICE:local}") private String serviceName; @Value("${K_REVISION:local}") private String revision; @Value("${REGION:us-central1}") private String region; public String getServiceName() { return serviceName; } public String getRevision() { return revision; } public String getRegion() { return region; } } 5. Domain Models
package com.example.cloudrun.model; import jakarta.persistence.*; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import java.time.LocalDateTime; import java.util.UUID; @Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; @NotBlank @Size(max = 100) @Column(nullable = false) private String firstName; @NotBlank @Size(max = 100) @Column(nullable = false) private String lastName; @NotBlank @Email @Size(max = 255) @Column(nullable = false, unique = true) private String email; @Enumerated(EnumType.STRING) @Column(nullable = false) private UserStatus status = UserStatus.ACTIVE; @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; @Column(name = "updated_at", nullable = false) private LocalDateTime updatedAt; @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); updatedAt = LocalDateTime.now(); } @PreUpdate protected void onUpdate() { updatedAt = LocalDateTime.now(); } // Constructors public User() {} public User(String firstName, String lastName, String email) { this.firstName = firstName; this.lastName = lastName; this.email = email; } // Getters and Setters public UUID getId() { return id; } public void setId(UUID id) { this.id = id; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public UserStatus getStatus() { return status; } public void setStatus(UserStatus status) { this.status = status; } public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } public LocalDateTime getUpdatedAt() { return updatedAt; } public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } } enum UserStatus { ACTIVE, INACTIVE, SUSPENDED } // Data Transfer Objects public class UserResponse { private final UUID id; private final String firstName; private final String lastName; private final String email; private final UserStatus status; private final LocalDateTime createdAt; public UserResponse(User user) { this.id = user.getId(); this.firstName = user.getFirstName(); this.lastName = user.getLastName(); this.email = user.getEmail(); this.status = user.getStatus(); this.createdAt = user.getCreatedAt(); } // Getters public UUID getId() { return id; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public String getEmail() { return email; } public UserStatus getStatus() { return status; } public LocalDateTime getCreatedAt() { return createdAt; } } public class CreateUserRequest { @NotBlank @Size(max = 100) private String firstName; @NotBlank @Size(max = 100) private String lastName; @NotBlank @Email @Size(max = 255) private String email; // Getters and Setters public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } } 6. Repository Layer
package com.example.cloudrun.repository; import com.example.cloudrun.model.User; import com.example.cloudrun.model.UserStatus; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; import java.util.UUID; @Repository public interface UserRepository extends JpaRepository<User, UUID> { Optional<User> findByEmail(String email); List<User> findByStatus(UserStatus status); Page<User> findByStatus(UserStatus status, Pageable pageable); @Query("SELECT u FROM User u WHERE LOWER(u.firstName) LIKE LOWER(CONCAT('%', :name, '%')) OR LOWER(u.lastName) LIKE LOWER(CONCAT('%', :name, '%'))") Page<User> findByNameContainingIgnoreCase(@Param("name") String name, Pageable pageable); boolean existsByEmail(String email); long countByStatus(UserStatus status); } 7. Service Layer
package com.example.cloudrun.service; import com.example.cloudrun.model.User; import com.example.cloudrun.model.UserStatus; import com.example.cloudrun.repository.UserRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Optional; import java.util.UUID; @Service @Transactional public class UserService { private static final Logger LOG = LoggerFactory.getLogger(UserService.class); private final UserRepository userRepository; private final CloudRunMetrics metrics; public UserService(UserRepository userRepository, CloudRunMetrics metrics) { this.userRepository = userRepository; this.metrics = metrics; } @Transactional(readOnly = true) public Page<User> getAllUsers(Pageable pageable) { LOG.debug("Fetching users page: {}", pageable); metrics.recordUserQuery(); return userRepository.findAll(pageable); } @Transactional(readOnly = true) @Cacheable(value = "users", key = "#id") public Optional<User> getUserById(UUID id) { LOG.debug("Fetching user by ID: {}", id); metrics.recordUserQuery(); return userRepository.findById(id); } @Transactional(readOnly = true) public Optional<User> getUserByEmail(String email) { LOG.debug("Fetching user by email: {}", email); metrics.recordUserQuery(); return userRepository.findByEmail(email); } public User createUser(User user) { LOG.info("Creating new user with email: {}", user.getEmail()); if (userRepository.existsByEmail(user.getEmail())) { throw new UserServiceException("User with email " + user.getEmail() + " already exists"); } User savedUser = userRepository.save(user); metrics.recordUserCreated(); LOG.info("Successfully created user: {}", savedUser.getId()); return savedUser; } public User updateUser(UUID id, User userUpdate) { LOG.info("Updating user: {}", id); User existingUser = userRepository.findById(id) .orElseThrow(() -> new UserServiceException("User not found: " + id)); // Update fields if provided if (userUpdate.getFirstName() != null) { existingUser.setFirstName(userUpdate.getFirstName()); } if (userUpdate.getLastName() != null) { existingUser.setLastName(userUpdate.getLastName()); } if (userUpdate.getStatus() != null) { existingUser.setStatus(userUpdate.getStatus()); } User updatedUser = userRepository.save(existingUser); metrics.recordUserUpdated(); LOG.info("Successfully updated user: {}", id); return updatedUser; } public void deleteUser(UUID id) { LOG.info("Deleting user: {}", id); User user = userRepository.findById(id) .orElseThrow(() -> new UserServiceException("User not found: " + id)); userRepository.delete(user); metrics.recordUserDeleted(); LOG.info("Successfully deleted user: {}", id); } @Transactional(readOnly = true) public long getActiveUsersCount() { long count = userRepository.countByStatus(UserStatus.ACTIVE); metrics.recordUserQuery(); return count; } public User deactivateUser(UUID id) { LOG.info("Deactivating user: {}", id); User user = userRepository.findById(id) .orElseThrow(() -> new UserServiceException("User not found: " + id)); user.setStatus(UserStatus.INACTIVE); User deactivatedUser = userRepository.save(user); metrics.recordUserDeactivated(); return deactivatedUser; } } class UserServiceException extends RuntimeException { public UserServiceException(String message) { super(message); } public UserServiceException(String message, Throwable cause) { super(message, cause); } } 8. REST Controllers
package com.example.cloudrun.controller; import com.example.cloudrun.model.CreateUserRequest; import com.example.cloudrun.model.User; import com.example.cloudrun.model.UserResponse; import com.example.cloudrun.service.UserService; import jakarta.validation.Valid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.UUID; @RestController @RequestMapping("/api/v1/users") public class UserController { private static final Logger LOG = LoggerFactory.getLogger(UserController.class); private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @GetMapping public ResponseEntity<Page<UserResponse>> getUsers( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size, @RequestParam(defaultValue = "createdAt") String sortBy, @RequestParam(defaultValue = "desc") String sortDirection) { LOG.info("Fetching users - page: {}, size: {}, sort: {}", page, size, sortBy); Sort sort = Sort.by(Sort.Direction.fromString(sortDirection), sortBy); Pageable pageable = PageRequest.of(page, size, sort); Page<UserResponse> users = userService.getAllUsers(pageable) .map(UserResponse::new); return ResponseEntity.ok(users); } @GetMapping("/{id}") public ResponseEntity<UserResponse> getUserById(@PathVariable UUID id) { LOG.info("Fetching user by ID: {}", id); return userService.getUserById(id) .map(user -> ResponseEntity.ok(new UserResponse(user))) .orElse(ResponseEntity.notFound().build()); } @PostMapping public ResponseEntity<UserResponse> createUser(@Valid @RequestBody CreateUserRequest request) { LOG.info("Creating user with email: {}", request.getEmail()); User user = new User(request.getFirstName(), request.getLastName(), request.getEmail()); User createdUser = userService.createUser(user); return ResponseEntity.status(HttpStatus.CREATED) .body(new UserResponse(createdUser)); } @PutMapping("/{id}") public ResponseEntity<UserResponse> updateUser( @PathVariable UUID id, @Valid @RequestBody CreateUserRequest request) { LOG.info("Updating user: {}", id); User userUpdate = new User(request.getFirstName(), request.getLastName(), request.getEmail()); User updatedUser = userService.updateUser(id, userUpdate); return ResponseEntity.ok(new UserResponse(updatedUser)); } @DeleteMapping("/{id}") public ResponseEntity<Void> deleteUser(@PathVariable UUID id) { LOG.info("Deleting user: {}", id); userService.deleteUser(id); return ResponseEntity.noContent().build(); } @PostMapping("/{id}/deactivate") public ResponseEntity<UserResponse> deactivateUser(@PathVariable UUID id) { LOG.info("Deactivating user: {}", id); User deactivatedUser = userService.deactivateUser(id); return ResponseEntity.ok(new UserResponse(deactivatedUser)); } @GetMapping("/stats/active-count") public ResponseEntity<ActiveUsersCountResponse> getActiveUsersCount() { long count = userService.getActiveUsersCount(); return ResponseEntity.ok(new ActiveUsersCountResponse(count)); } } class ActiveUsersCountResponse { private final long activeUsersCount; public ActiveUsersCountResponse(long activeUsersCount) { this.activeUsersCount = activeUsersCount; } public long getActiveUsersCount() { return activeUsersCount; } } 9. Google Cloud Integration Services
package com.example.cloudrun.integration; import com.google.cloud.storage.BlobId; import com.google.cloud.storage.BlobInfo; import com.google.cloud.storage.Storage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.io.InputStream; import java.nio.channels.Channels; import java.util.UUID; @Service public class CloudStorageService { private static final Logger LOG = LoggerFactory.getLogger(CloudStorageService.class); private final Storage storage; @Value("${gcp.storage.bucket:default-bucket}") private String bucketName; public CloudStorageService(Storage storage) { this.storage = storage; } public String uploadFile(InputStream fileStream, String contentType, String originalFilename) { try { String fileName = generateFileName(originalFilename); BlobId blobId = BlobId.of(bucketName, fileName); BlobInfo blobInfo = BlobInfo.newBuilder(blobId) .setContentType(contentType) .build(); storage.createFrom(blobInfo, fileStream); String publicUrl = String.format("https://storage.googleapis.com/%s/%s", bucketName, fileName); LOG.info("File uploaded successfully: {}", publicUrl); return publicUrl; } catch (Exception e) { LOG.error("Failed to upload file to Cloud Storage: {}", e.getMessage(), e); throw new CloudStorageException("File upload failed", e); } } public void deleteFile(String fileName) { try { boolean deleted = storage.delete(bucketName, fileName); if (deleted) { LOG.info("File deleted successfully: {}", fileName); } else { LOG.warn("File not found for deletion: {}", fileName); } } catch (Exception e) { LOG.error("Failed to delete file from Cloud Storage: {}", e.getMessage(), e); throw new CloudStorageException("File deletion failed", e); } } private String generateFileName(String originalFilename) { String extension = ""; if (originalFilename != null && originalFilename.contains(".")) { extension = originalFilename.substring(originalFilename.lastIndexOf(".")); } return "uploads/" + UUID.randomUUID() + extension; } } class CloudStorageException extends RuntimeException { public CloudStorageException(String message) { super(message); } public CloudStorageException(String message, Throwable cause) { super(message, cause); } } package com.example.cloudrun.integration; import com.google.cloud.secretmanager.v1.AccessSecretVersionResponse; import com.google.cloud.secretmanager.v1.SecretManagerServiceClient; import com.google.cloud.secretmanager.v1.SecretVersionName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.io.IOException; @Service public class SecretManagerService { private static final Logger LOG = LoggerFactory.getLogger(SecretManagerService.class); public String getSecret(String secretId, String version) { try (SecretManagerServiceClient client = SecretManagerServiceClient.create()) { String projectId = System.getenv("GOOGLE_CLOUD_PROJECT"); SecretVersionName secretVersionName = SecretVersionName.of(projectId, secretId, version); AccessSecretVersionResponse response = client.accessSecretVersion(secretVersionName); String secretValue = response.getPayload().getData().toStringUtf8(); LOG.debug("Successfully retrieved secret: {}", secretId); return secretValue; } catch (IOException e) { LOG.error("Failed to retrieve secret {}: {}", secretId, e.getMessage()); throw new SecretManagerException("Failed to retrieve secret: " + secretId, e); } } public String getSecret(String secretId) { return getSecret(secretId, "latest"); } } class SecretManagerException extends RuntimeException { public SecretManagerException(String message) { super(message); } public SecretManagerException(String message, Throwable cause) { super(message, cause); } } 10. Metrics and Monitoring
package com.example.cloudrun.metrics; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; @Component public class CloudRunMetrics { private final Counter requestsCounter; private final Counter userCreatedCounter; private final Counter userUpdatedCounter; private final Counter userDeletedCounter; private final Counter userDeactivatedCounter; private final Counter userQueryCounter; private final Timer requestTimer; public CloudRunMetrics(MeterRegistry meterRegistry) { this.requestsCounter = Counter.builder("cloudrun.requests.total") .description("Total number of HTTP requests") .register(meterRegistry); this.userCreatedCounter = Counter.builder("cloudrun.users.created") .description("Number of users created") .register(meterRegistry); this.userUpdatedCounter = Counter.builder("cloudrun.users.updated") .description("Number of users updated") .register(meterRegistry); this.userDeletedCounter = Counter.builder("cloudrun.users.deleted") .description("Number of users deleted") .register(meterRegistry); this.userDeactivatedCounter = Counter.builder("cloudrun.users.deactivated") .description("Number of users deactivated") .register(meterRegistry); this.userQueryCounter = Counter.builder("cloudrun.users.queries") .description("Number of user queries") .register(meterRegistry); this.requestTimer = Timer.builder("cloudrun.request.duration") .description("HTTP request duration") .register(meterRegistry); } public void recordRequest() { requestsCounter.increment(); } public void recordUserCreated() { userCreatedCounter.increment(); } public void recordUserUpdated() { userUpdatedCounter.increment(); } public void recordUserDeleted() { userDeletedCounter.increment(); } public void recordUserDeactivated() { userDeactivatedCounter.increment(); } public void recordUserQuery() { userQueryCounter.increment(); } public void recordRequestTime(long duration, TimeUnit unit) { requestTimer.record(duration, unit); } } Docker Configuration
1. Dockerfile for Cloud Run
# Use Eclipse Temurin JRE for smaller image size FROM eclipse-temurin:21-jre-jammy as runtime # Install curl for health checks RUN apt-get update && \ apt-get install -y curl && \ rm -rf /var/lib/apt/lists/* && \ apt-get clean # Create a non-root user RUN groupadd --system --gid 1000 appgroup && \ useradd --system --uid 1000 --gid appgroup appuser # Set working directory WORKDIR /app # Copy the JAR file COPY target/*.jar app.jar # Change ownership to non-root user RUN chown -R appuser:appgroup /app # Switch to non-root user USER appuser # Expose the port Cloud Run expects EXPOSE 8080 # Health check (Cloud Run uses startup and liveness probes) HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \ CMD curl -f http://localhost:8080/actuator/health || exit 1 # Optimized JVM flags for Cloud Run ENV JAVA_OPTS="-Xmx512m -Xms256m -XX:+UseG1GC -XX:MaxRAMPercentage=75.0 -Djava.security.egd=file:/dev/./urandom" # Start the application ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/app.jar"]
2. Jib Configuration for Maven
<!-- Enhanced Jib Configuration --> <plugin> <groupId>com.google.cloud.tools</groupId> <artifactId>jib-maven-plugin</artifactId> <version>3.4.0</version> <configuration> <from> <image>eclipse-temurin:21-jre-jammy</image> <platforms> <platform> <architecture>amd64</architecture> <os>linux</os> </platform> </platforms> </from> <to> <image>gcr.io/${google.cloud.project}/${project.artifactId}</image> <tags> <tag>${project.version}</tag> <tag>latest</tag> </tags> </to> <container> <user>1000:1000</user> <workingDirectory>/app</workingDirectory> <entrypoint> <arg>java</arg> <arg>-Xmx512m</arg> <arg>-Xms256m</arg> <arg>-XX:+UseG1GC</arg> <arg>-XX:MaxRAMPercentage=75.0</arg> <arg>-Djava.security.egd=file:/dev/./urandom</arg> <arg>-jar</arg> <arg>/app/app.jar</arg> </entrypoint> <ports> <port>8080</port> </ports> <environment> <SPRING_PROFILES_ACTIVE>cloudrun</SPRING_PROFILES_ACTIVE> <JAVA_TOOL_OPTIONS>-XX:+ExitOnOutOfMemoryError</JAVA_TOOL_OPTIONS> </environment> <creationTime>USE_CURRENT_TIMESTAMP</creationTime> <labels> <maintainer>[email protected]</maintainer> <version>${project.version}</version> <commit-sha>${git.commit.id.abbrev}</commit-sha> </labels> </container> </configuration> </plugin> Deployment Configuration
1. Cloud Run Service Definition
# service.yaml apiVersion: serving.knative.dev/v1 kind: Service metadata: name: cloudrun-demo labels: app: cloudrun-demo environment: production spec: template: metadata: annotations: # Auto-scaling configuration autoscaling.knative.dev/maxScale: "100" autoscaling.knative.dev/minScale: "0" # Concurrency configuration run.googleapis.com/concurrency: "80" # CPU configuration run.googleapis.com/cpu-throttling: "true" # Startup configuration run.googleapis.com/startup-cpu-boost: "true" spec: containerConcurrency: 80 timeoutSeconds: 300 containers: - image: gcr.io/YOUR_PROJECT_ID/cloudrun-demo:latest ports: - containerPort: 8080 resources: limits: cpu: 1000m memory: 512Mi requests: cpu: 250m memory: 256Mi env: - name: SPRING_PROFILES_ACTIVE value: "cloudrun,production" - name: DATABASE_URL valueFrom: secretKeyRef: name: cloudrun-db-secret key: url - name: DATABASE_USERNAME valueFrom: secretKeyRef: name: cloudrun-db-secret key: username - name: DATABASE_PASSWORD valueFrom: secretKeyRef: name: cloudrun-db-secret key: password - name: REDIS_HOST value: "10.0.0.3" - name: MAX_INSTANCES value: "100" - name: CONCURRENCY value: "80" - name: TIMEOUT_SECONDS value: "300" startupProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 0 periodSeconds: 5 timeoutSeconds: 10 failureThreshold: 30 # 30 * 5 = 150 seconds max startup time livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 10 periodSeconds: 30 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 3 traffic: - percent: 100 latestRevision: true
2. Deployment Scripts
#!/bin/bash # deploy.sh set -e # Configuration PROJECT_ID="your-project-id" SERVICE_NAME="cloudrun-demo" REGION="us-central1" IMAGE_NAME="gcr.io/$PROJECT_ID/$SERVICE_NAME" echo "Building and deploying $SERVICE_NAME to Cloud Run..." # Build the container image echo "Building container image..." mvn compile jib:build -DskipTests # Deploy to Cloud Run echo "Deploying to Cloud Run..." gcloud run deploy $SERVICE_NAME \ --image $IMAGE_NAME \ --platform managed \ --region $REGION \ --allow-unauthenticated \ --memory 512Mi \ --cpu 1 \ --concurrency 80 \ --max-instances 100 \ --timeout 300s \ --set-env-vars=SPRING_PROFILES_ACTIVE=cloudrun,production \ --update-secrets=DATABASE_URL=cloudrun-db-secret:latest \ --update-secrets=DATABASE_USERNAME=cloudrun-db-secret:latest \ --update-secrets=DATABASE_PASSWORD=cloudrun-db-secret:latest # Get the service URL SERVICE_URL=$(gcloud run services describe $SERVICE_NAME --region $REGION --format 'value(status.url)') echo "Service deployed successfully: $SERVICE_URL" # Run health check echo "Running health check..." curl -f "$SERVICE_URL/actuator/health" || exit 1 echo "Health check passed!"
3. Cloud Build Configuration
# cloudbuild.yaml steps: # Build the application - name: maven:3.9-eclipse-temurin-21 entrypoint: mvn args: ['clean', 'compile', '-DskipTests'] # Run tests - name: maven:3.9-eclipse-temurin-21 entrypoint: mvn args: ['test'] # Build container image - name: maven:3.9-eclipse-temurin-21 entrypoint: mvn args: ['compile', 'jib:build', '-DskipTests'] secretEnv: ['GOOGLE_APPLICATION_CREDENTIALS'] # Deploy to Cloud Run - name: 'gcr.io/cloud-builders/gcloud' args: - 'run' - 'deploy' - 'cloudrun-demo' - '--image=gcr.io/$PROJECT_ID/cloudrun-demo:latest' - '--region=us-central1' - '--platform=managed' - '--allow-unauthenticated' - '--memory=512Mi' - '--cpu=1' - '--concurrency=80' - '--max-instances=100' - '--timeout=300s' - '--set-env-vars=SPRING_PROFILES_ACTIVE=cloudrun,production' - '--update-secrets=DATABASE_URL=cloudrun-db-secret:latest' - '--update-secrets=DATABASE_USERNAME=cloudrun-db-secret:latest' - '--update-secrets=DATABASE_PASSWORD=cloudrun-db-secret:latest' # Secrets configuration availableSecrets: secretManager: - versionName: projects/$PROJECT_ID/secrets/cloudbuild-sa-key/versions/latest env: 'GOOGLE_APPLICATION_CREDENTIALS' # Images to store in Container Registry images: - 'gcr.io/$PROJECT_ID/cloudrun-demo:latest' # Timeout and options timeout: 1800s options: logging: CLOUD_LOGGING_ONLY
Testing
1. Unit Tests
package com.example.cloudrun.service; import com.example.cloudrun.model.User; import com.example.cloudrun.model.UserStatus; import com.example.cloudrun.repository.UserRepository; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.Optional; import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock private UserRepository userRepository; @Mock private CloudRunMetrics metrics; @InjectMocks private UserService userService; @Test void testCreateUser() { // Given User user = new User("John", "Doe", "[email protected]"); User savedUser = new User("John", "Doe", "[email protected]"); savedUser.setId(UUID.randomUUID()); when(userRepository.existsByEmail("[email protected]")).thenReturn(false); when(userRepository.save(any(User.class))).thenReturn(savedUser); // When User result = userService.createUser(user); // Then assertNotNull(result); assertNotNull(result.getId()); assertEquals("[email protected]", result.getEmail()); verify(userRepository).existsByEmail("[email protected]"); verify(userRepository).save(user); verify(metrics).recordUserCreated(); } @Test void testGetUserById() { // Given UUID userId = UUID.randomUUID(); User user = new User("John", "Doe", "[email protected]"); user.setId(userId); when(userRepository.findById(userId)).thenReturn(Optional.of(user)); // When Optional<User> result = userService.getUserById(userId); // Then assertTrue(result.isPresent()); assertEquals(userId, result.get().getId()); verify(userRepository).findById(userId); verify(metrics).recordUserQuery(); } } 2. Integration Tests
package com.example.cloudrun.controller; import com.example.cloudrun.model.CreateUserRequest; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") class UserControllerIntegrationTest { @Autowired private MockMvc mockMvc; private final ObjectMapper objectMapper = new ObjectMapper(); @Test void testCreateUser() throws Exception { CreateUserRequest request = new CreateUserRequest(); request.setFirstName("John"); request.setLastName("Doe"); request.setEmail("[email protected]"); mockMvc.perform(post("/api/v1/users") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()) .andExpect(jsonPath("$.firstName").value("John")) .andExpect(jsonPath("$.lastName").value("Doe")) .andExpect(jsonPath("$.email").value("[email protected]")); } @Test void testHealthCheck() throws Exception { mockMvc.perform(get("/actuator/health")) .andExpect(status().isOk()) .andExpect(jsonPath("$.status").value("UP")); } } 3. Test Configuration
# application-test.yml spring: datasource: url: jdbc:tc:postgresql:15:///testdb username: test password: test jpa: hibernate: ddl-auto: create-drop show-sql: true redis: host: localhost port: 6379 cloud: gcp: enabled: false logging: level: com.example.cloudrun: DEBUG
Performance Optimization
1. Cold Start Optimization
package com.example.cloudrun.optimization; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.CommandLineRunner; import org.springframework.context.annotation.Profile; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; import javax.sql.DataSource; import java.sql.Connection; @Component @Profile("cloudrun") public class WarmupRunner implements CommandLineRunner { private static final Logger LOG = LoggerFactory.getLogger(WarmupRunner.class); private final DataSource dataSource; private final JdbcTemplate jdbcTemplate; private final RedisTemplate<String, String> redisTemplate; public WarmupRunner(DataSource dataSource, JdbcTemplate jdbcTemplate, RedisTemplate<String, String> redisTemplate) { this.dataSource = dataSource; this.jdbcTemplate = jdbcTemplate; this.redisTemplate = redisTemplate; } @Override public void run(String... args) throws Exception { LOG.info("Starting application warmup..."); // Warm up database connections warmupDatabase(); // Warm up Redis connection warmupRedis(); // Warm up any other critical components warmupComponents(); LOG.info("Application warmup completed"); } private void warmupDatabase() { try (Connection connection = dataSource.getConnection()) { jdbcTemplate.execute("SELECT 1"); LOG.info("Database connection warmed up successfully"); } catch (Exception e) { LOG.warn("Database warmup failed: {}", e.getMessage()); } } private void warmupRedis() { try { redisTemplate.opsForValue().get("warmup"); LOG.info("Redis connection warmed up successfully"); } catch (Exception e) { LOG.warn("Redis warmup failed: {}", e.getMessage()); } } private void warmupComponents() { // Warm up any other components that benefit from initialization // This could include HTTP clients, thread pools, etc. LOG.info("Component warmup completed"); } } 2. Memory Optimization Configuration
package com.example.cloudrun.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; @Configuration @EnableAsync public class AsyncConfiguration { @Bean(name = "taskExecutor") @ConditionalOnProperty(name = "spring.threads.virtual.enabled", havingValue = "false") public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // Conservative settings for Cloud Run executor.setCorePoolSize(2); executor.setMaxPoolSize(10); executor.setQueueCapacity(50); executor.setThreadNamePrefix("CloudRunAsync-"); executor.setWaitForTasksToCompleteOnShutdown(true); executor.setAwaitTerminationSeconds(30); executor.initialize(); return executor; } } Monitoring and Logging
1. Structured Logging
package com.example.cloudrun.logging; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.UUID; @Component public class RequestLoggingFilter extends OncePerRequestFilter { private static final Logger LOG = LoggerFactory.getLogger(RequestLoggingFilter.class); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String requestId = UUID.randomUUID().toString(); MDC.put("requestId", requestId); MDC.put("service", "cloudrun-demo"); long startTime = System.currentTimeMillis(); try { LOG.info("Request started: {} {} from {}", request.getMethod(), request.getRequestURI(), request.getRemoteAddr()); filterChain.doFilter(request, response); } finally { long duration = System.currentTimeMillis() - startTime; LOG.info("Request completed: {} {} - Status: {} - Duration: {}ms", request.getMethod(), request.getRequestURI(), response.getStatus(), duration); MDC.clear(); } } } 2. Custom Metrics
package com.example.cloudrun.metrics; import io.micrometer.core.instrument.Gauge; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; import java.util.concurrent.atomic.AtomicInteger; @Component public class RuntimeMetrics { private final AtomicInteger activeRequests = new AtomicInteger(0); private final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); public RuntimeMetrics(MeterRegistry meterRegistry) { // Active requests gauge Gauge.builder("cloudrun.runtime.active.requests") .description("Number of active HTTP requests") .register(meterRegistry, activeRequests, AtomicInteger::get); // Memory usage gauge Gauge.builder("cloudrun.runtime.memory.used") .description("Used memory in bytes") .register(meterRegistry, this, RuntimeMetrics::getUsedMemory); // Available memory gauge Gauge.builder("cloudrun.runtime.memory.max") .description("Max memory in bytes") .register(meterRegistry, this, RuntimeMetrics::getMaxMemory); } public void incrementActiveRequests() { activeRequests.incrementAndGet(); } public void decrementActiveRequests() { activeRequests.decrementAndGet(); } private long getUsedMemory() { return memoryMXBean.getHeapMemoryUsage().getUsed() + memoryMXBean.getNonHeapMemoryUsage().getUsed(); } private long getMaxMemory() { return memoryMXBean.getHeapMemoryUsage().getMax() + memoryMXBean.getNonHeapMemoryUsage().getMax(); } @Scheduled(fixedRate = 30000) // Every 30 seconds public void logRuntimeStats() { Runtime runtime = Runtime.getRuntime(); long usedMemory = runtime.totalMemory() - runtime.freeMemory(); long maxMemory = runtime.maxMemory(); LOG.info("Runtime stats - Memory: {}/{} MB, Active requests: {}", usedMemory / 1024 / 1024, maxMemory / 1024 / 1024, activeRequests.get()); } } Best Practices
1. Environment-specific Configuration
# application-production.yml spring: datasource: hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 30000 max-lifetime: 1800000 idle-timeout: 600000 redis: lettuce: pool: max-active: 16 max-idle: 8 min-idle: 2 jpa: properties: hibernate: connection: provider_disables_autocommit: true management: endpoints: web: exposure: include: health,info,metrics endpoint: health: show-details: never metrics: enabled: true logging: level: com.example.cloudrun: INFO org.hibernate: WARN org.springframework: INFO # Cloud Run specific optimizations spring: lifecycle: timeout-per-shutdown-phase: 30s threads: virtual: enabled: true
2. Security Configuration
package com.example.cloudrun.security; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfiguration { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // Cloud Run handles SSL termination .requiresChannel(channel -> channel.anyRequest().requiresSecure()) // CORS configuration .cors(cors -> cors.configure(http)) // CSRF configuration (stateless API) .csrf(csrf -> csrf.disable()) // Authorization rules .authorizeHttpRequests(authz -> authz .requestMatchers("/actuator/health", "/actuator/info").permitAll() .requestMatchers("/api/v1/**").authenticated() .anyRequest().denyAll() ); return http.build(); } } Conclusion
Building Java applications for Google Cloud Run provides:
- Serverless operation with automatic scaling
- Cost efficiency with pay-per-use pricing
- Zero infrastructure management
- Fast deployment and iteration cycles
- Integration with Google Cloud services
Key optimization strategies covered:
- Container optimization with Jib and efficient base images
- Cold start reduction through warmup and JVM tuning
- Resource management with appropriate memory and CPU limits
- Health checks for reliable deployment and scaling
- Monitoring and logging with Cloud Operations
- Database and cache integration for performance
- Security best practices for production deployment
This architecture enables building highly scalable, cost-effective Java applications that automatically scale based on demand while maintaining excellent performance and reliability.