Skip to content
This repository has been archived by the owner on Jun 23, 2021. It is now read-only.

Back end

James Hood edited this page Oct 14, 2019 · 13 revisions

It is very common for applications to handle user requests, process data, and respond back with success or error. These users have to get authenticated, and require ways to communicate with APIs (usually via a web interface).

For example, the create-application operation of the service is exposed via an Amazon API Gateway endpoint. Customers interact with the service using the AWS SDK and the AWS console, where auth(n) and auth(z) is managed via AWS IAM. Requests made to the service get routed to AWS Lambda where the business logic is executed, and state is managed via Amazon DynamoDB. The open source project captures the request/response architecture of the production service almost identically, except that we show how to use Amazon Cognito for authentication and authorization. The following diagram captures the architecture for the back-end component released with this open source project.

Backend architecture

The back-end component implements the following operations:

  • CreateApplication: creates an application
  • UpdateApplication: updates the application metadata
  • GetApplication: gets the details of an application
  • ListApplications: lists applications that you have created
  • DeleteApplication: deletes an application

We used the Open API’s (Swagger) specification to define our APIs, and used the Swagger code-gen tool to generate server side models (for input/output), and JAX-RS annotations for the APIs listed above.

A quick word about JAX-RS (and Jersey)

JAX-RS (Java API for RESTful Web Services) is a Java programming language API spec that provides support in creating web services according to the Representational State Transfer architectural pattern. Jersey, the reference implementation of JAX-RS, implements support for the annotations defined in JSR 311, making it easy for developers to build RESTful web services by using the Java programming language.

Code Snippets

Let’s walk through a few sections of code that help show how requests get routed and then processed by the appropriate Java methods of the application. It will be helpful to see how we used existing Java frameworks to help developers stay focused on writing just the business logic of the application.

The code snippet below shows JAX-RS annotations defined for the create-application API in the ApplicationsApi Java Interface generated from the API spec.

@javax.annotation.Generated(value = "io.swagger.codegen.v3.generators.java.JavaJAXRSSpecServerCodegen", date = "2019-10-10T15:53:41.375-07:00[America/Los_Angeles]")
public interface ApplicationsApi {

    @POST
    @Consumes({ "application/json" })
    @Produces({ "application/json" })
    @Operation(summary = "", description = "", security = {
        @SecurityRequirement(name = "cognitoAuthorizer")    }, tags={  })
        @ApiResponses(value = { 
        @ApiResponse(responseCode = "201", description = "Successfully Created an application.", content = @Content(schema = @Schema(implementation = Application.class))),
        @ApiResponse(responseCode = "400", description = "Bad Request Exception", content = @Content(schema = @Schema(implementation = BadRequestException.class))),
        @ApiResponse(responseCode = "401", description = "Unauthorized Exception", content = @Content(schema = @Schema(implementation = UnauthorizedException.class))),
        @ApiResponse(responseCode = "409", description = "Conflict Exception", content = @Content(schema = @Schema(implementation = ConflictException.class))),
        @ApiResponse(responseCode = "429", description = "Too Many Requests Exception", content = @Content(schema = @Schema(implementation = TooManyRequestsException.class))),
        @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = InternalServerErrorException.class))) })
    Application createApplication(@Valid CreateApplicationInput body);

The ApplicationService class contains all business logic for the APIs. This class implements the methods defined by the ApplicationsApi Java interface. The code snippet below shows the implementation of the create-application API: a simple Java method that accepts the CreateApplicationInput POJO as input to process the request for creating an application.

  @Override
  public Application createApplication(final CreateApplicationInput createApplicationInput) {
    log.info("Creating application with input {}", createApplicationInput);
    ApplicationRecord applicationRecord = modelMapper.map(createApplicationInput,
          ApplicationRecord.class);
    applicationRecord.setCreatedAt(Instant.now(clock));
    applicationRecord.setVersion(1L);
    applicationRecord.setUserId(securityContext.getUserPrincipal().getName());
    try {
      dynamodb.putItem(PutItemRequest.builder()
            .tableName(tableName)
            .item(applicationRecord.toAttributeMap())
            .conditionExpression(
                  String.format("attribute_not_exists(%s) AND attribute_not_exists(%s)",
                        ApplicationRecord.USER_ID_ATTRIBUTE_NAME,
                        ApplicationRecord.APPLICATION_ID_ATTRIBUTE_NAME))
            .build());
    } catch (ConditionalCheckFailedException e) {
      throw new ConflictApiException(new ConflictException()
            .errorCode("ApplicationAlreadyExist")
            .message(String.format("Application %s already exists.",
                  createApplicationInput.getApplicationId())));
    }
    return modelMapper.map(applicationRecord, Application.class);
  }

Finally, the code snippet below shows how Amazon API Gateway requests get routed (from the Lambda handler) to the appropriate Java methods in the ApplicationService class.

As mentioned earlier, the project uses the Jersey framework (an implementation of JAX-RS). The ApplicationService class is registered with ResourceConfig (a Jersey Application) so that Jersey can forward REST calls to the appropriate methods in ApplicationService class. The API requests get sent to Jersey via the JerseyLambdaContainerHandler middle ware component that natively supports API Gateway's proxy integration models for requests and responses. This middle ware component is part of the open source AWS Serverless Java Container framework which provides Java wrappers to run Jersey, Spring, Spark, and other Java-based framework apps inside AWS Lambda.

/**
 * API Lambda handler. This is the entry point for the API Lambda.
 */
public class ApiLambdaHandler implements RequestStreamHandler {
  private static final ResourceConfig jerseyApplication = new ResourceConfig()
        .registerClasses(ApplicationsService.class, ApiExceptionMapper.class,
                CorsHeadersResponseFilter.class)
        .register(JacksonFeature.class)
        .register(new AbstractBinder() {
          @Override
          protected void configure() {
            bindFactory(DynamoDbClientFactory.class)
                  .to(DynamoDbClient.class).in(Singleton.class);
            bindFactory(SsmConfigProviderFactory.class)
                  .to(ConfigProvider.class).in(Singleton.class);
            bindFactory(KmsClientFactory.class)
                  .to(KmsClient.class).in(Singleton.class);
            bind(PaginationTokenSerializer.class)
                  .to(new TypeLiteral<TokenSerializer<Map<String, AttributeValue>>>() {
                  })
                  .in(Singleton.class);
          }
        });

  private static final JerseyLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler
        = JerseyLambdaContainerHandler.getAwsProxyHandler(jerseyApplication);

  @Override
  public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context)
        throws IOException {
    handler.proxyStream(inputStream, outputStream, context);
  }
}

Application Templates

For deployment to AWS, we use AWS Serverless Application Model (SAM), which provides shorthand syntax to express functions, APIs, databases, and event source mappings. These templates can be deployed manually or as part of an automated pipeline. When organizing our templates, we adopted two key best practices learned over the years at AWS: nested stacks and parameter referencing:

  1. Nested stacks make the templates reusable across different stacks. As your infrastructure grows, common patterns can be converted into templates, and these templates can be defined as resources in other templates. In this project, we have added a root level SAM template called template.yaml for each component. Within that template, nested templates, such as database.template.yaml and api.template.yaml, are defined as resources. These nested templates, in turn, define the specific resources required by the component, such as an API Gateway API and a DynamoDB table.

  2. For parameter referencing we used AWS Systems Manager Parameter store, which allows configurable parameters to be stored in a parameter store and referenced inside a SAM template. A parameter store also allows referencing these directly inside Lambda functions instead of passing them as environment variables. It allows managing the configuration values independent of your service deployment.

Running Integration Tests

Integration tests for the back-end are included in this project and run as part of the CD pipeline when any changes are made. You can also run the integration tests manually.

First, you will need an Amazon S3 bucket to run the integration tests. If you don't have one, you can run the following AWS CLI command to create one:

aws s3 mb s3://<your S3 bucket name>

The S3 bucket will store the packaged artifacts, and AWS CloudFormation will use them for deployment.

Then run the following commands from the repository root directory to run the integration tests:

cd backend
mvn clean verify -DpackageBucket=<your S3 bucket name>

The command will run the following three integration test phases:

  1. pre-integration-test: deploys the service into your AWS account as a test CloudFormation stack. It also deploys a stack to bring up the resources that are needed by the integration tests.
  2. integration-test: runs the integration tests against the created Amazon API Gateway endpoint of the test CloudFormation stack.
  3. post-integration-test: deletes the CloudFormation stacks created in the pre-integration-test phase.