1. OVERVIEW

Amazon SNS (Simple Notification Service) is a fully managed topic-based messaging service that helps publishing notifications for subscriber/listener applications to process.

Amazon SNS follows the pub/sub model to facilitate asynchronous communication between different systems.

SNS supports multiple delivery protocols such as:

  • http/https,
  • email,
  • sms
  • AWS SQS
  • AWS Lambda
  • AWS Kinesis Data Firehose
  • mobile applications

This blog post helps you to get started publishing notifications with Spring Boot 2 or 3, SNS, Spring Cloud AWS, and AWS Java SDK version 2, and consuming the messages via SQS with AWS CLI.

Spring Boot, Spring Cloud AWS, and SNS Spring Boot, Spring Cloud AWS, and SNS

2. SNS AND DOCKER

This blog post uses LocalStack’s localstack Docker image to run an SNS instance.

Let’s first add this AWS profile to your local AWS credentials file:

~/.aws/credentials:

[localstack-localdev]
aws_access_key_id = testAccessKey
aws_secret_access_key = testSecretKey
region = localhost

These settings are good enough for local development purposes.

Let’s run an SNS Docker container:

docker run -d -p 4566:4566 --name=localstack-messaging -e SERVICES=sns,sqs -e AWS_DEFAULT_REGION=localhost localstack/localstack:latest

The command sets the environment variable SERVICES=sns,sqs.
The sns service supports publishing SNS notifications.
The sqs service is needed to create an SQS queue and subscribe it to the SNS topic the demo Spring Boot application publishes notifications to.

Once an SQS queue subscribes to the SNS topic, you can use AWS CLI or write another Spring Boot application to process fanned out SNS notifications.

You can now interact with the SNS instance using AWS CLI:

aws --profile=localstack-localdev sns list-topics --endpoint-url=http://localhost:4566
{
  "Topics": []
}

Notice the command uses the AWS profile just added.

This is a new SNS instance.
Let’s create a new topic topic-customer-creation-events a RESTful Spring Boot application will publish notifications to.

aws --profile=localstack-localdev sns create-topic --name=topic-customer-creation-events --endpoint-url=http://localhost:4566
{
  "TopicArn": "arn:aws:sns:us-east-1:000000000000:topic-customer-creation-events"
}

It creates a new topic named topic-customer-creation-events, but it doesn’t have any subscriber yet.

aws --profile=localstack-localdev sns list-subscriptions --endpoint-url=http://localhost:4566
{
    "Subscriptions": []
}

Let’s create an SQS queue and subscribe it to the SNS topic.

aws --profile=localstack-localdev sqs create-queue --queue-name=queue-customers-sns --endpoint-url=http://localhost:4566
{
    "QueueUrl": "http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/queue-customers-sns"
}

It creates a new SQS queue named queue-customers-sns with QueueUrl set to http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/queue-customers-sns. These are values the Spring Boot application will use in a configuration file.

You first need to know the SQS queue QueueArn attribute before subscribing it to the SNS topic.

aws --profile=localstack-localdev sqs get-queue-attributes --queue-url=http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/queue-customers-sns --attribute-names=QueueArn --endpoint-url=http://localhost:4566
{
    "Attributes": {
        "QueueArn": "arn:aws:sqs:us-east-1:000000000000:queue-customers-sns"
    }
}

The command parameter queue-url is set to the output of the create SQS queue command.

Let’s now subscribe the SQS queue to the SNS topic.

aws --profile=localstack-localdev sns subscribe --topic-arn=arn:aws:sns:us-east-1:000000000000:topic-customer-creation-events --protocol=sqs --notification-endpoint=arn:aws:sqs:us-east-1:000000000000:queue-customers-sns --endpoint-url=http://localhost:4566
{
    "SubscriptionArn": "arn:aws:sns:us-east-1:000000000000:topic-customer-creation-events:db9d7d29-8458-4d96-97f9-09ab7e9bbee0"
}

The command parameter topic-arn is set to the output of the create topic command.
The parameter protocol is set to sqs, meaning the listener endpoint is an SQS queue.
The parameter notification-endpoint is set to the QueueArn attribute retrieved earlier.

The SNS instance is now setup and ready for development.

3. MAVEN DEPENDENCIES

pom.xml:

...
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.2.11</version>
  <relativePath />
</parent>

<properties>
  <java.version>21</java.version>
  <spring-cloud-aws.version>3.2.1</spring-cloud-aws.version>
</properties>

<dependencies>
  <dependency>
    <groupId>io.awspring.cloud</groupId>
    <artifactId>spring-cloud-aws-starter-sns</artifactId>
  </dependency>
</dependencies>

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>io.awspring.cloud</groupId>
      <artifactId>spring-cloud-aws-dependencies</artifactId>
      <version>${spring-cloud-aws.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
  • This Spring Boot application uses version 3.2.11.

  • AWS BOM’s spring-cloud-aws-dependencies is set to 3.2.1.
    This is the most recent version you can use with Spring Boot 3.2.x according to the compatibility table below.

  • spring-cloud-aws-starter-sns is one of the libraries managed by spring-cloud-aws-dependencies.
    I’ll use it to send notifications to an SNS topic.

Spring Boot, Spring Cloud AWS, and AWS Java SDK compatibility table

Spring Cloud AWS Spring Boot Spring Cloud AWS Java SDK
3.0.x 3.0.x, 3.1.x 2022.0.x (4.0/Kilburn) 2.x
3.1.x 3.2.x 2023.0.x (4.0/Kilburn) 2.x
3.2.0, 3.2.1 3.2.x, 3.3.x 2023.0.x (4.0/Kilburn) 2.x

Souce: https://github.com/awspring/spring-cloud-aws

4. SNS NOTIFICATION SERVICE CLASS

SnsNotificationService.java:

@Service
public class SnsNotificationService implements NotificationService {

  @Value("${customer-creation-events.topic-arn}")
  private String destinationTopic;

  private final SnsTemplate snsTemplate;

  @Override
  public <T> void sendNotification(T payload) {
    // This could be sent async
    Map<String, Object> headers = this.headers();
    this.snsTemplate.convertAndSend(this.destinationTopic, payload, headers);
  }
// ...
}

This is a simple implementation that sends a generic payload object to the target SNS topic using the SnsTemplate bean.

A further improvement could be sending the SNS notifications asynchronously, either using ThreadPoolTaskExecutor or VirtualThreadTaskExecutor.

5. REST CONTROLLER CLASS

CustomerController.java:

@RestController
@RequestMapping(value = "/api/customers", produces = MediaType.APPLICATION_JSON_VALUE)
public class CustomerController {

  private final NotificationService notificationService;

  @PostMapping(value = "", consumes = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<String> createCustomer(@RequestBody Customer customer) {
    // You might want to pass a CustomerDto to decouple the external model from the internal domain.

    // Do something with Customer internal domain
    // ...

    // You might want to map from the Customer entity to a CustomerDto instance to prevent exposing the internal domain
    // as the external interface

    // Publish notification
    // This could be part of the business logic
    this.notificationService.sendNotification(customer);

    // But let's return a String for simplicity
    return ResponseEntity.ok().body("OK");
  }

}

The controller sends the request payload to the configured SNS topic.

Normally, you would send SNS notifications as part of the business logic implementation. I wrote it this way to keep the blog post simple.

6. CONFIGURATION

application.yml:

spring:
  cloud:
    aws:
      region:
        static: localhost
      credentials:
        accessKey: testAccessKey
        secretKey: testSecretKey
      sns:
        endpoint: http://localhost:4566/

customer-creation-events:
  topic-arn: arn:aws:sns:us-east-1:000000000000:topic-customer-creation-events

The spring.cloud.aws.* properties match those added in the AWS credentials localstack-localdev profile.

The spring.cloud.aws.sns.endpoint property is set to the Localstack Docker host’s IP, port.

The customer-creation-events.topic-arn property is set to SNS topic’s TopicArn attribute created earlier, and used in the SnsNotificationService class.

Localstack Docker container Localstack Docker container

7. PUBLISHING, CONSUMING SNS NOTIFICATIONS

As a final exercise, let’s send a POST request to the /api/customers endpoint.

curl -H "Content-Type: application/json" -H "Accept: application/json" -X POST http://localhost:8080/api/customers -d '
> {
>   "firstName": "Orlando",
>   "lastName": "Otero",
>   "emailAddress": "invalid.1@asimiotech.com",
>   "phone": {
>     "number": "123-456-7890",
>     "type": "MOBILE"
>   },
>   "mailingAddress": {
>     "street": "123 Main St",
>     "city": "Orlando",
>     "state": "FL",
>     "zipcode": "32801"
>   }
> }'
OK

The POST /api/customers implementation sends an SNS notification to a topic that is bridged to an SQS queue.

Let’s read the message with AWS CLI.

aws --profile=localstack-localdev sqs receive-message --queue-url=http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/queue-customers-sns --endpoint-url=http://localhost:4566
{
  "Messages": [
    {
      "MessageId": "b7ff0065-f6b3-4269-998d-377d65d97c57",
      "ReceiptHandle": "YmVlOTU0N2UtMzU0My00YzA4LTg3OGYtNmYzMjVkNmY5OTZjIGFybjphd3M6c3FzOnVzLWVhc3QtMTowMDAwMDAwMDAwMDA6cXVldWUtY3VzdG9tZXJzLXNucyBiN2ZmMDA2NS1mNmIzLTQyNjktOTk4ZC0zNzdkNjVkOTdjNTcgMTczMTYzMTE4NS44NzIyMTkz",
      "MD5OfBody": "ff134ef97740fcfb76a6c95814ccf649",
      "Body": "{\"Type\": \"Notification\", \"MessageId\": \"61402e66-25d9-46bf-b411-7b00b6120764\", \"TopicArn\": \"arn:aws:sns:us-east-1:000000000000:topic-customer-creation-events\", \"Message\": \"{\\\"id\\\":null,\\\"firstName\\\":\\\"Orlando\\\",\\\"lastName\\\":\\\"Otero\\\",\\\"emailAddress\\\":\\\"invalid.1@asimiotech.com\\\",\\\"phone\\\":{\\\"number\\\":\\\"123-456-7890\\\",\\\"type\\\":\\\"MOBILE\\\"},\\\"mailingAddress\\\":{\\\"street\\\":\\\"123 Main St\\\",\\\"suite\\\":null,\\\"city\\\":\\\"Orlando\\\",\\\"state\\\":\\\"FL\\\",\\\"zipcode\\\":\\\"32801\\\"}}\", \"Timestamp\": \"2024-11-15T00:39:13.065Z\", \"UnsubscribeURL\": \"http://localhost.localstack.cloud:4566/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:000000000000:topic-customer-creation-events:4674c5a3-9414-4c3f-94e1-f4b2046109a4\", \"MessageAttributes\": {\"blah-header-name\": {\"Type\": \"String\", \"Value\": \"blah-header-value\"}, \"id\": {\"Type\": \"String\", \"Value\": \"5984091a-ae37-c257-f8ed-c0cc8b3e4400\"}, \"contentType\": {\"Type\": \"String\", \"Value\": \"application/json\"}, \"timestamp\": {\"Type\": \"Number.java.lang.Long\", \"Value\": \"1731631152916\"}}, \"SignatureVersion\": \"1\", \"Signature\": \"P5gBOZhgj4FVaodlC0xLrJdRCsYim+uTIr0Nn9U4tFCq6ulqjvJ3ep9grByQAj6ngROhCQKG1BN3VvK5MT+mcg0IFngIqwsgwWPSykVGUyycVBqK1y3s7Aor5uk838E7XJoOyh0hKAhorDwW/I5BcbrKbora67FJqIbBUI3aP6PnBemcU3GgVPdqjMMNSNuptmZr3SYApJqpY4aIZj4MRnaiDX6nxembpI/jyTs+N4kUzzf7k7tE3SPCUIq+Wg4oZOn+O97lbSHfeATpQriB6o81tOrr7vk+eFMakDQg0lm1hHoqTQQry3LyFW5qnI/mBrt9sVDIUH81MbxTOuQ9Qw==\", \"SigningCertURL\": \"http://localhost.localstack.cloud:4566/_aws/sns/SimpleNotificationService-6c6f63616c737461636b69736e696365.pem\"}"
    }
  ]
}

Note the Body attribute is in text format.
It also includes the Message value set to the request payload submitted earlier, again, in text format.

You can now delete the message from the SQS queue using the Message’s ReceiptHandle attribute value.

aws --profile=localstack-localdev sqs delete-message --queue-url=http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/queue-customers-sns --receipt-handle='
YmVlOTU0N2UtMzU0My00YzA4LTg3OGYtNmYzMjVkNmY5OTZjIGFybjphd3M6c3FzOnVzLWVhc3QtMTowMDAwMDAwMDAwMDA6cXVldWUtY3VzdG9tZXJzLXNucyBiN2ZmMDA2NS1mNmIzLTQyNjktOTk4ZC0zNzdkNjVkOTdjNTcgMTczMTYzMTE4NS44NzIyMTkz
' --endpoint-url=http://localhost:4566

or delete/purge all messages from the SQS queue.

aws --profile=localstack-localdev sqs purge-queue --queue-url=http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/queue-customers-sns --endpoint-url=http://localhost:4566

8. CONCLUSION

This blog post covered how to get started sending Amazon SNS notifications with Spring Boot, spring-cloud-aws-starter-sns, and AWS Java SDK version 2.

It also included AWS CLI commands to provision an SNS topic, an SQS queue, and an SNS/SQS subscription to read the messages from.
All these hosted in a Localstack Docker container for local development.

Thanks for reading and as always, feedback is very much appreciated. If you found this post helpful and would like to receive updates when content like this gets published, sign up to the newsletter.

9. SOURCE CODE

Your organization can now save time and costs by purchasing a working code base, clean implementation, with support for future revisions.