AWS Lambda is a serverless compute service that runs your code in response to events and automatically manages the underlying compute resources. Java is a fully supported runtime for Lambda functions, offering strong typing, rich ecosystem, and excellent performance.
Lambda Handler Types
Basic Handler Implementations
// Simple request/response handler public class SimpleHandler implements RequestHandler<Map<String, String>, String> { private static final Logger logger = LoggerFactory.getLogger(SimpleHandler.class); @Override public String handleRequest(Map<String, String> input, Context context) { logger.info("Processing request with input: {}", input); String name = input.getOrDefault("name", "World"); return String.format("Hello, %s!", name); } } // Using custom POJO for input/output public class UserHandler implements RequestHandler<UserRequest, UserResponse> { @Override public UserResponse handleRequest(UserRequest request, Context context) { LambdaLogger logger = context.getLogger(); logger.log("Processing user: " + request.getUserId()); // Business logic User user = userService.findUser(request.getUserId()); return UserResponse.builder() .userId(user.getId()) .name(user.getName()) .email(user.getEmail()) .status("SUCCESS") .build(); } } // POJO classes public class UserRequest { private String userId; // getters and setters public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } } public class UserResponse { private String userId; private String name; private String email; private String status; // builder pattern public static Builder builder() { return new Builder(); } public static class Builder { private UserResponse response = new UserResponse(); public Builder userId(String userId) { response.userId = userId; return this; } public Builder name(String name) { response.name = name; return this; } public Builder email(String email) { response.email = email; return this; } public Builder status(String status) { response.status = status; return this; } public UserResponse build() { return response; } } // getters public String getUserId() { return userId; } public String getName() { return name; } public String getEmail() { return email; } public String getStatus() { return status; } } Stream Handler for Raw Input
public class StreamHandler implements RequestStreamHandler { private static final ObjectMapper objectMapper = new ObjectMapper(); private final UserService userService; public StreamHandler() { this.userService = new UserService(); } @Override public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException { LambdaLogger logger = context.getLogger(); try { // Parse input directly from stream UserRequest request = objectMapper.readValue(input, UserRequest.class); logger.log("Processing request for user: " + request.getUserId()); // Process request User user = userService.findUser(request.getUserId()); UserResponse response = UserResponse.builder() .userId(user.getId()) .name(user.getName()) .email(user.getEmail()) .status("SUCCESS") .build(); // Write response to output stream objectMapper.writeValue(output, response); } catch (Exception e) { logger.log("Error processing request: " + e.getMessage()); ErrorResponse error = ErrorResponse.builder() .errorCode("PROCESSING_ERROR") .message(e.getMessage()) .build(); objectMapper.writeValue(output, error); } } } Dependency Injection with Spring Cloud Function
// Spring Cloud Function setup @SpringBootApplication public class LambdaApplication { @Bean public Function<UserRequest, UserResponse> getUser() { return request -> { UserService userService = new UserService(); User user = userService.findUser(request.getUserId()); return UserResponse.builder() .userId(user.getId()) .name(user.getName()) .email(user.getEmail()) .status("SUCCESS") .build(); }; } @Bean public Consumer<Event> processEvent() { return event -> { // Process event without return value eventProcessor.process(event); }; } @Bean public Supplier<List<User>> getUsers() { return () -> userService.getAllUsers(); } } // With dependency injection @Service public class UserService { private final UserRepository userRepository; private final AuditService auditService; public UserService(UserRepository userRepository, AuditService auditService) { this.userRepository = userRepository; this.auditService = auditService; } public User findUser(String userId) { return userRepository.findById(userId) .orElseThrow(() -> new UserNotFoundException("User not found: " + userId)); } } @Configuration public class AppConfig { @Bean public UserRepository userRepository() { return new DynamoDBUserRepository(); } @Bean public AuditService auditService() { return new CloudWatchAuditService(); } } Advanced Lambda Patterns
Pattern 1: S3 Event Processing
public class S3ImageProcessor implements RequestHandler<S3Event, String> { private static final Logger logger = LoggerFactory.getLogger(S3ImageProcessor.class); private final AmazonS3 s3Client; private final ImageProcessingService imageService; public S3ImageProcessor() { this.s3Client = AmazonS3ClientBuilder.defaultClient(); this.imageService = new ImageProcessingService(); } @Override public String handleRequest(S3Event s3Event, Context context) { logger.info("Processing S3 event with {} records", s3Event.getRecords().size()); for (S3EventNotificationRecord record : s3Event.getRecords()) { String bucketName = record.getS3().getBucket().getName(); String key = record.getS3().getObject().getKey(); logger.info("Processing image: {}/{}", bucketName, key); try { // Download image from S3 S3Object s3Object = s3Client.getObject(bucketName, key); InputStream imageStream = s3Object.getObjectContent(); // Process image ProcessedImage processed = imageService.resizeImage(imageStream, 800, 600); // Upload processed image String processedKey = "processed/" + key; s3Client.putObject(bucketName, processedKey, new ByteArrayInputStream(processed.getData()), new ObjectMetadata()); logger.info("Successfully processed image: {}", processedKey); } catch (Exception e) { logger.error("Failed to process image: {}/{}", bucketName, key, e); // Consider DLQ for failed processing } } return String.format("Processed %d images", s3Event.getRecords().size()); } } Pattern 2: API Gateway Proxy Integration
public class ApiGatewayHandler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> { private final ObjectMapper objectMapper = new ObjectMapper(); private final UserService userService; public ApiGatewayHandler() { this.userService = new UserService(); } @Override public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent request, Context context) { LambdaLogger logger = context.getLogger(); try { String httpMethod = request.getHttpMethod(); String path = request.getPath(); logger.log("Processing " + httpMethod + " " + path); switch (httpMethod) { case "GET": return handleGetRequest(request); case "POST": return handlePostRequest(request); case "PUT": return handlePutRequest(request); case "DELETE": return handleDeleteRequest(request); default: return createErrorResponse(405, "Method not allowed"); } } catch (Exception e) { logger.log("Error processing request: " + e.getMessage()); return createErrorResponse(500, "Internal server error"); } } private APIGatewayProxyResponseEvent handleGetRequest(APIGatewayProxyRequestEvent request) { String path = request.getPath(); if (path.matches("/users/.+")) { // Extract user ID from path String userId = path.substring(path.lastIndexOf("/") + 1); User user = userService.findUser(userId); return createSuccessResponse(user); } else if (path.equals("/users")) { // Get all users with query parameters Map<String, String> queryParams = request.getQueryStringParameters(); String role = queryParams != null ? queryParams.get("role") : null; List<User> users = userService.getUsersByRole(role); return createSuccessResponse(users); } return createErrorResponse(404, "Not found"); } private APIGatewayProxyResponseEvent handlePostRequest(APIGatewayProxyRequestEvent request) { if (request.getPath().equals("/users")) { try { UserCreateRequest createRequest = objectMapper.readValue( request.getBody(), UserCreateRequest.class); User user = userService.createUser(createRequest); return createSuccessResponse(201, user); } catch (Exception e) { return createErrorResponse(400, "Invalid request body"); } } return createErrorResponse(404, "Not found"); } private APIGatewayProxyResponseEvent createSuccessResponse(Object body) { return createSuccessResponse(200, body); } private APIGatewayProxyResponseEvent createSuccessResponse(int statusCode, Object body) { try { String responseBody = objectMapper.writeValueAsString(body); return new APIGatewayProxyResponseEvent() .withStatusCode(statusCode) .withBody(responseBody) .withHeaders(Map.of( "Content-Type", "application/json", "Access-Control-Allow-Origin", "*" )); } catch (Exception e) { return createErrorResponse(500, "Error serializing response"); } } private APIGatewayProxyResponseEvent createErrorResponse(int statusCode, String message) { Map<String, String> errorResponse = Map.of( "error", message, "statusCode", String.valueOf(statusCode) ); try { String responseBody = objectMapper.writeValueAsString(errorResponse); return new APIGatewayProxyResponseEvent() .withStatusCode(statusCode) .withBody(responseBody) .withHeaders(Map.of( "Content-Type", "application/json", "Access-Control-Allow-Origin", "*" )); } catch (Exception e) { // Fallback if JSON serialization fails return new APIGatewayProxyResponseEvent() .withStatusCode(500) .withBody("{\"error\":\"Internal server error\"}") .withHeaders(Map.of("Content-Type", "application/json")); } } } Pattern 3: DynamoDB Stream Processing
public class DynamoDBStreamProcessor implements RequestHandler<DynamodbEvent, String> { private static final Logger logger = LoggerFactory.getLogger(DynamoDBStreamProcessor.class); private final AuditService auditService; private final NotificationService notificationService; public DynamoDBStreamProcessor() { this.auditService = new AuditService(); this.notificationService = new NotificationService(); } @Override public String handleRequest(DynamodbEvent dynamodbEvent, Context context) { logger.info("Processing DynamoDB stream with {} records", dynamodbEvent.getRecords().size()); int processedCount = 0; int failedCount = 0; for (DynamodbStreamRecord record : dynamodbEvent.getRecords()) { try { processRecord(record); processedCount++; } catch (Exception e) { logger.error("Failed to process record: {}", record.getEventID(), e); failedCount++; } } logger.info("Processed {} records, {} failed", processedCount, failedCount); return String.format("Processed: %d, Failed: %d", processedCount, failedCount); } private void processRecord(DynamodbStreamRecord record) { String eventName = record.getEventName(); Map<String, AttributeValue> newImage = record.getDynamodb().getNewImage(); Map<String, AttributeValue> oldImage = record.getDynamodb().getOldImage(); switch (eventName) { case "INSERT": handleInsert(newImage); break; case "MODIFY": handleModify(oldImage, newImage); break; case "REMOVE": handleRemove(oldImage); break; default: logger.warn("Unknown event type: {}", eventName); } } private void handleInsert(Map<String, AttributeValue> newImage) { String userId = newImage.get("userId").getS(); String userName = newImage.get("name").getS(); // Log audit trail auditService.logEvent("USER_CREATED", userId, Map.of("userName", userName)); // Send welcome notification notificationService.sendWelcomeEmail(userId, userName); logger.info("Processed new user: {} - {}", userId, userName); } private void handleModify(Map<String, AttributeValue> oldImage, Map<String, AttributeValue> newImage) { String userId = newImage.get("userId").getS(); String oldName = oldImage.get("name").getS(); String newName = newImage.get("name").getS(); if (!oldName.equals(newName)) { // Log name change auditService.logEvent("USER_UPDATED", userId, Map.of("oldName", oldName, "newName", newName)); logger.info("User {} changed name from {} to {}", userId, oldName, newName); } } private void handleRemove(Map<String, AttributeValue> oldImage) { String userId = oldImage.get("userId").getS(); String userName = oldImage.get("name").getS(); // Log deletion auditService.logEvent("USER_DELETED", userId, Map.of("userName", userName)); logger.info("User deleted: {} - {}", userId, userName); } } Performance Optimization
Cold Start Optimization
// Initialization outside handler public class OptimizedHandler implements RequestHandler<UserRequest, UserResponse> { // Static initialization for cold start optimization private static final UserService userService; private static final ObjectMapper objectMapper; private static final DynamoDBMapper dynamoDBMapper; static { // Initialize expensive resources once userService = new UserService(); objectMapper = new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .registerModule(new JavaTimeModule()); // Initialize AWS clients AmazonDynamoDB client = AmazonDynamoDBClientBuilder.standard() .withRegion(Regions.US_EAST_1) .build(); dynamoDBMapper = new DynamoDBMapper(client); // Warm up dependencies userService.initialize(); } @Override public UserResponse handleRequest(UserRequest request, Context context) { // Handler logic uses pre-initialized components return userService.processUser(request); } } // Connection pooling for databases @Component public class DatabaseManager { private static HikariDataSource dataSource; static { HikariConfig config = new HikariConfig(); config.setJdbcUrl(System.getenv("DB_URL")); config.setUsername(System.getenv("DB_USERNAME")); config.setPassword(System.getenv("DB_PASSWORD")); config.setMaximumPoolSize(5); // Conservative for Lambda config.setMinimumIdle(2); config.setConnectionTimeout(30000); config.setIdleTimeout(300000); config.setMaxLifetime(900000); dataSource = new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } } Environment Configuration
public class ConfigManager { private static final Map<String, String> config = new HashMap<>(); static { // Load environment variables config.put("database_url", System.getenv("DATABASE_URL")); config.put("s3_bucket", System.getenv("S3_BUCKET")); config.put("stage", System.getenv("STAGE")); // Load from AWS Systems Manager Parameter Store loadParametersFromSSM(); } private static void loadParametersFromSSM() { AWSSimpleSystemsManagement ssm = AWSSimpleSystemsManagementClientBuilder.defaultClient(); String path = "/app/" + config.get("stage") + "/"; GetParametersByPathRequest request = new GetParametersByPathRequest() .withPath(path) .withRecursive(true) .withWithDecryption(true); GetParametersByPathResult result = ssm.getParametersByPath(request); for (Parameter param : result.getParameters()) { String key = param.getName().replace(path, ""); config.put(key, param.getValue()); } } public static String get(String key) { return config.get(key); } public static String get(String key, String defaultValue) { return config.getOrDefault(key, defaultValue); } } Testing Lambda Functions
Unit Testing
class LambdaHandlerTest { private TestContext context; private UserHandler handler; @BeforeEach void setUp() { handler = new UserHandler(); context = new TestContext(); } @Test void testHandleRequest_Success() { // Given UserRequest request = new UserRequest(); request.setUserId("123"); // When UserResponse response = handler.handleRequest(request, context); // Then assertThat(response.getUserId()).isEqualTo("123"); assertThat(response.getStatus()).isEqualTo("SUCCESS"); } @Test void testHandleRequest_UserNotFound() { // Given UserRequest request = new UserRequest(); request.setUserId("nonexistent"); // When/Then assertThatThrownBy(() -> handler.handleRequest(request, context)) .isInstanceOf(UserNotFoundException.class); } } // Test context implementation class TestContext implements Context { @Override public String getAwsRequestId() { return "test-request-id"; } @Override public String getLogGroupName() { return "test-log-group"; } @Override public String getLogStreamName() { return "test-log-stream"; } @Override public String getFunctionName() { return "test-function"; } @Override public String getFunctionVersion() { return "1.0"; } @Override public String getInvokedFunctionArn() { return "test-arn"; } @Override public CognitoIdentity getIdentity() { return null; } @Override public ClientContext getClientContext() { return null; } @Override public int getRemainingTimeInMillis() { return 30000; } @Override public int getMemoryLimitInMB() { return 512; } @Override public LambdaLogger getLogger() { return message -> System.out.println("LOG: " + message); } } Integration Testing
@SpringBootTest @TestPropertySource(properties = { "AWS_ACCESS_KEY_ID=test", "AWS_SECRET_ACCESS_KEY=test", "AWS_REGION=us-east-1" }) class LambdaIntegrationTest { @Localstack private LocalStackContainer localstack; @Autowired private Function<UserRequest, UserResponse> getUserFunction; @BeforeEach void setup() { // Setup test data in local DynamoDB setupTestData(); } @Test void testGetUserFunction() { // Given UserRequest request = new UserRequest(); request.setUserId("test-user-1"); // When UserResponse response = getUserFunction.apply(request); // Then assertThat(response.getUserId()).isEqualTo("test-user-1"); assertThat(response.getName()).isEqualTo("Test User"); } private void setupTestData() { // Use localstack to setup test data // ... } } Deployment Configuration
SAM Template (template.yaml)
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Parameters: Stage: Type: String Default: dev AllowedValues: - dev - staging - prod Globals: Function: Timeout: 30 MemorySize: 512 Runtime: java11 Environment: Variables: STAGE: !Ref Stage Resources: UserFunction: Type: AWS::Serverless::Function Properties: FunctionName: !Sub user-service-${Stage} Handler: com.example.UserHandler::handleRequest CodeUri: target/user-service-1.0.0.jar Policies: - DynamoDBCrudPolicy: TableName: !Ref UsersTable - S3ReadPolicy: BucketName: !Ref DocumentsBucket Events: Api: Type: Api Properties: Path: /users/{userId} Method: get S3Event: Type: S3 Properties: Bucket: !Ref DocumentsBucket Events: s3:ObjectCreated:* UsersTable: Type: AWS::DynamoDB::Table Properties: TableName: !Sub users-${Stage} BillingMode: PAY_PER_REQUEST AttributeDefinitions: - AttributeName: userId AttributeType: S KeySchema: - AttributeName: userId KeyType: HASH DocumentsBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub documents-${Stage}-${AWS::AccountId} Outputs: UserFunctionArn: Description: "User Function ARN" Value: !GetAtt UserFunction.Arn ApiUrl: Description: "API Gateway URL" Value: !Sub https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/users Build Configuration (Maven)
<!-- pom.xml --> <project> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>user-service</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <aws.lambda.java.version>1.2.1</aws.lambda.java.version> <aws.java.sdk.version>2.20.0</aws.java.sdk.version> </properties> <dependencies> <dependency> <groupId>com.amazonaws</groupId> <artifactId>aws-lambda-java-core</artifactId> <version>${aws.lambda.java.version}</version> </dependency> <dependency> <groupId>com.amazonaws</groupId> <artifactId>aws-lambda-java-events</artifactId> <version>3.11.0</version> </dependency> <dependency> <groupId>software.amazon.awssdk</groupId> <artifactId>dynamodb</artifactId> <version>${aws.java.sdk.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.3.0</version> <configuration> <createDependencyReducedPom>false</createDependencyReducedPom> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </xml> Monitoring and Logging
public class MonitoringHandler implements RequestHandler<UserRequest, UserResponse> { private static final MeterRegistry meterRegistry = CloudWatchMeterRegistry.builder() .namespace("MyApp") .build(); @Override public UserResponse handleRequest(UserRequest request, Context context) { Timer.Sample timer = Timer.start(meterRegistry); Counter requests = meterRegistry.counter("requests.total"); try { requests.increment(); // Business logic UserResponse response = processRequest(request); // Record success metric meterRegistry.counter("requests.success").increment(); return response; } catch (Exception e) { // Record error metric meterRegistry.counter("requests.error").increment(); throw e; } finally { // Record execution time timer.stop(Timer.builder("requests.duration") .register(meterRegistry)); } } // Custom metrics utility public static void recordBusinessMetric(String metricName, double value) { meterRegistry.gauge(metricName, value); } } Best Practices
1. Performance
- Initialize expensive resources outside handler
- Use connection pooling for databases
- Keep deployment package size minimal
- Use provisioned concurrency for critical functions
2. Security
- Use IAM roles with least privilege
- Encrypt environment variables
- Validate input thoroughly
- Use VPC for sensitive operations
3. Reliability
- Implement proper error handling
- Use dead letter queues (DLQ) for async invocations
- Set appropriate timeouts and memory
- Implement retry logic with exponential backoff
4. Observability
- Use structured logging
- Include correlation IDs
- Monitor cold start times
- Track business metrics
Conclusion
AWS Lambda with Java provides a powerful platform for serverless applications:
- Cost-effective: Pay only for compute time used
- Scalable: Automatic scaling based on demand
- Managed infrastructure: No server management required
- Rich ecosystem: Integration with AWS services
Key success factors:
- Proper initialization for cold start optimization
- Efficient resource usage within Lambda constraints
- Comprehensive monitoring and logging
- Security best practices for IAM and data protection
Java Lambda functions are ideal for:
- API backends with API Gateway
- Event processing from S3, DynamoDB, Kinesis
- Data transformation pipelines
- Scheduled tasks with EventBridge
- Microservices in serverless architectures