Serverless Functions with AWS Lambda in Java

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

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper