Skip to content

Commit

Permalink
Merge pull request #6 from tjschutte/main
Browse files Browse the repository at this point in the history
Create Java version of Credit-Card-Product service, and switch application to use it.
  • Loading branch information
tjschutte authored Oct 10, 2024
2 parents 73a56cf + 3c6ac92 commit 091f9da
Show file tree
Hide file tree
Showing 63 changed files with 1,623 additions and 15 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ infrastructure/templates/pgt-proxy/docker_image_builder_*
infrastructure/templates/application/docker_image_builder_*
application/frontend/node_modules
application/frontend/run.sh
application/java-credit-card-product-service/target
*.jar*
*.class
*.lst
81 changes: 80 additions & 1 deletion README.MD
Original file line number Diff line number Diff line change
@@ -1,11 +1,90 @@
# Notes

This repo is for the Ambar Event Sourcing / Microservices courses.
You can use this repo to deploy a real world credit card application which leverages both event sourcing and microservices
to create a larger application. Note that this is an academic example, implemented in multiple languages and with multiple
components - you should not expect to deeply understand everything in all the languages.

## Preparation Steps

These steps will be performed with your instructor during the first coding session of your course, and will allow you to
deploy this repo into a cloud environment running on Google Cloud Platform (GCP)

1. Fork this repository
2. Create two GitHub action secrets (provided by your course instructor) as follows:
```
STATE_MANAGEMENT_BASE64=some_string_here
CREDENTIALS_BASE64=some_string_here
```
3. Make a blank commit, and push it to GitHub.
**N.B. Make sure to enter the entire string for each secret, with no quotes, and no newlines!**
3. Make a trivial commit (such as updating this readme), and push it to GitHub.

## Extending this program

Our sample application attempts to model a financial institution which offers different credit cards (products). We have
already modeled some services of the application - some multiple times in multiple languages! To get started, you should
look at one of the already modeled services like the Credit-Card-Product service which is implemented in multiple languages
and try to follow along in one of the languages you are familiar with (php, java). Reimplement the service in your language
of choice, or add any missing features to the module.

Once you have an understanding of the pattern as it works in your chosen implementation language, feel free to implement
another service of the application. If you feel inclined, open a pull request to the Ambar repo, and we will review and merge
it and extend the frontend application to leverage the capabilities!

### Event Sourcing Additional Context / Reminders

A quick reminder on Event Sourcing

Event Sourcing is the inversion of state in an application. Instead of directly storing state in a record and performing CRUD
operations on those records, we record series of events that describe what happened in our application and derive state from
one or more of those events. This gives us a direct historical event sequence that we can use to build up state in many ways
and to even 'time travel' to see what state was in the past, without the need for audit tables or trying to deal with the
dual write problem.

Note: This does not mean we do not directly store state in event sourcing! We can still distill information from our events
to build useful models about the state of our application and its data. This is where projections and read models come in.

An easy way to organize event sourcing conceptually, and in code, is by leveraging the Command Query Responsibility Separation
pattern (CQRS) where we create distinct paths for our write (record events) and read (leverage events / state) actions.

#### Terms refresher

* **Command**: A request for something to happen in our application.
* E.G. Making a new credit card product for customers to apply for.
* **Event**: A recording of something significant happening in our application containing things like who, what, and when.
Events should be small and precise, and are written sequentially to an immutable log. Never change events or insert new ones
except at the tail of the log. Events should belong to short-lived processes, such as handling a request or system notification.
* E.G. Activating a credit card product so customers can apply for it
(Who: The product ID, What: Make it active, When: when was the event recorded)
* **Aggregate**: Information Model built from a set of related events used to model a process such has handling a command / request.
They should be small, and leverage event ordering to create a minified state for process handling.
* E.G. A Product Aggregate to model a credit card which is offered in our service. We can leverage the aggregate to determine
if a product is already available (active) or not, to determine if a command (request) is valid.
* **Projection**: Projection leverages building up some useful state from a filtered set of events and storing the derived
state into a projection database. These projections can then be leveraged during command validations when determining if
a command should be accepted (event written) or not.
* **Query**: Queries are the read side of our application, and leverage the models created by projections to retrieve information
store about the state of our application.

```
Comamnd (Read from EventStore/ReadModelStore, write back to EventStore)
-> [Command (Request)] -> [Build some state (EventStore Aggregate)] -> [Perform Validations (Aggregate, ReadModels)] ->
[Record whatever happened (New Event)] -> [EventStore]
(Strongly Consistent) ^
------------------------------------------------------------------------------------------------------------------------
(Eventually Consistent) v
Reaction (Event from EventStore, write back to EventStore)
-> [EventStore(Event)] -> [Build some state (EventStore Aggregate)] -> [Perform Validations (Aggregate, ReadModels)] ->
[Record whatever happened (New Event)] -> [EventStore]
Projections (Read from ReadModelStore, write to ReadModelStore)
-> [EventStore(Event)] -> [Build some state] -> [Project some interesting state] -> [ReadModelStore]
Querys (Read Commands) (Read from ReadModelStore)
-> [ReadModelStore (Modeled State)]
```

### Microservices Additional Context / Reminders
2 changes: 2 additions & 0 deletions application/java-credit-card-product-service/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
code/vendor
code/development-environment
15 changes: 15 additions & 0 deletions application/java-credit-card-product-service/CreditCardProduct.iml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="FacetManager">
<facet type="jpa" name="JPA">
<configuration>
<setting name="validation-enabled" value="true" />
<setting name="provider-name" value="Hibernate" />
<datasource-mapping>
<factory-entry name="entityManagerFactory" value="e2079969-38fd-4e93-8daa-97fd5223b996" />
</datasource-mapping>
<naming-strategy-map />
</configuration>
</facet>
</component>
</module>
30 changes: 30 additions & 0 deletions application/java-credit-card-product-service/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Step 1: Use Maven image to build the application
FROM maven:3.9.5-eclipse-temurin-21 AS build

# Set the working directory inside the container
WORKDIR /app

# Copy the pom.xml and download dependencies (layer caching)
COPY pom.xml .
RUN mvn dependency:go-offline

# Copy the rest of the application source code
COPY src ./src

# Build the Spring Boot application
RUN mvn clean package -DskipTests

# Step 2: Use JDK 21 runtime image to run the application
FROM eclipse-temurin:21-jdk

# Set the working directory for the runtime container
WORKDIR /app

# Copy the JAR file from the build image to the runtime image
COPY --from=build /app/target/*.jar app.jar

# Expose port 8080 (Spring Boot default port)
EXPOSE 8080

# Set the entry point to run the application
ENTRYPOINT ["java", "-jar", "app.jar"]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
data
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
endpoint="localhost:8080"

# To create the Starter card
curl -X POST "${endpoint}/api/v1/credit_card_product/product" \
-H "Content-Type: application/json" \
-d '{
"productIdentifierForAggregateIdHash": "STARTER_CREDIT_CARD",
"name": "Starter",
"interestInBasisPoints": 1200,
"annualFeeInCents": 5000,
"paymentCycle": "monthly",
"creditLimitInCents": 50000,
"maxBalanceTransferAllowedInCents": 0,
"reward": "none",
"cardBackgroundHex": "#7fffd4"
}'

# To create the Platinum card
curl -X POST "${endpoint}/api/v1/credit_card_product/product" \
-H "Content-Type: application/json" \
-d '{
"productIdentifierForAggregateIdHash": "PLATINUM_CREDIT_CARD",
"name": "Platinum",
"interestInBasisPoints": 300,
"annualFeeInCents": 50000,
"paymentCycle": "monthly",
"creditLimitInCents": 500000,
"maxBalanceTransferAllowedInCents": 100000,
"reward": "points",
"cardBackgroundHex": "#E5E4E2"
}'

# To list the current card products
curl -X POST "${endpoint}/api/v1/credit_card_product/product/list-items" | jq .

productId=""
# Activate a product
curl -X POST "${endpoint}/api/v1/credit_card_product/product/activate/${productId}"

# Deactivate a product
curl -X POST "${endpoint}/api/v1/credit_card_product/product/deactivate/${productId}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/bash
set -e

#docker run --rm -v $(pwd):/app composer:2.2.24 install --ignore-platform-reqs
docker compose down
docker compose up -d --build --force-recreate
sleep 5
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash
set -e

docker compose down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
services:

java-credit-card-product:
container_name: java-credit-card-product
build:
context: ./../
volumes:
- ./../:/srv
restart: always
environment:
EVENT_STORE_HOST: "172.30.0.102"
EVENT_STORE_PORT: 5432
EVENT_STORE_DATABASE_NAME: "my_es_database"
EVENT_STORE_USER: "my_es_username"
EVENT_STORE_PASSWORD: "my_es_password"
EVENT_STORE_CREATE_TABLE_WITH_NAME: "event_store"
EVENT_STORE_CREATE_REPLICATION_USER_WITH_USERNAME: "replication_username_test"
EVENT_STORE_CREATE_REPLICATION_USER_WITH_PASSWORD: "replication_password_test"
EVENT_STORE_CREATE_REPLICATION_PUBLICATION: "replication_publication_test"
MONGODB_PROJECTION_HOST: "172.30.0.103"
MONGODB_PROJECTION_PORT: 27017
MONGODB_PROJECTION_AUTHENTICATION_DATABASE: "admin"
MONGODB_PROJECTION_DATABASE_NAME: "projections"
MONGODB_PROJECTION_DATABASE_USERNAME: "my_mongo_username"
MONGODB_PROJECTION_DATABASE_PASSWORD: "my_mongo_password"
SESSION_TOKENS_EXPIRE_AFTER_SECONDS: 3600
depends_on:
- pg-event-store
- mongo-projection-reaction
networks:
TestNetwork:
ipv4_address: 172.30.0.101
deploy:
resources:
limits:
cpus: '0.500'
memory: 1024M

pg-event-store:
build:
context:
./pg-event-store
container_name: pg-event-store
restart: always
volumes:
- ./data/event-store/pg-data:/var/lib/postgresql/data
environment:
POSTGRES_USER: my_es_username
POSTGRES_DB: my_es_database
POSTGRES_PASSWORD: my_es_password
command: >
postgres -c wal_level=logical
-c ssl=on
-c ssl_cert_file=/var/lib/postgresql/certs/server.crt
-c ssl_key_file=/var/lib/postgresql/certs/server.key
expose:
- 5432
networks:
TestNetwork:
ipv4_address: 172.30.0.102

mongo-projection-reaction:
build:
context:
./mongo-projection-reaction
container_name: mongo-projection-reaction
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: my_mongo_username
MONGO_INITDB_ROOT_PASSWORD: my_mongo_password
volumes:
- ./data/mongo-projection-reaction/db-data:/data/db
expose:
- 27017
networks:
TestNetwork:
ipv4_address: 172.30.0.103

networks:
TestNetwork:
driver: bridge
ipam:
config:
- subnet: 172.30.0.0/24
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM mongo:7.0.14
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM postgres:16.4

RUN mkdir -p /var/lib/postgresql/certs
RUN openssl genrsa -out /var/lib/postgresql/certs/server.key 2048
RUN chmod 600 /var/lib/postgresql/certs/server.key
RUN openssl req -new -key /var/lib/postgresql/certs/server.key -out /var/lib/postgresql/certs/server.csr -subj "/C=GB/ST=London/L=London/O=SnakeOil/OU=Org/CN=snake-oil.oil"
RUN openssl x509 -req -in /var/lib/postgresql/certs/server.csr -signkey /var/lib/postgresql/certs/server.key -out /var/lib/postgresql/certs/server.crt -days 36500
RUN chown 999 /var/lib/postgresql/certs
RUN chown 999 /var/lib/postgresql/certs -Rf
79 changes: 79 additions & 0 deletions application/java-credit-card-product-service/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cloud.ambar</groupId>
<artifactId>CreditCardProduct</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>CreditCardProduct</name>
<description>Ambar Credit Card Product service for course demo</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Java Persistence API -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- Database Drivers -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>

<!-- Lombok for reducing boilerplate code -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>compile</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${project.parent.version}</version>
</plugin>
</plugins>
</build>

</project>
Loading

0 comments on commit 091f9da

Please sign in to comment.