Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/maven-docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Locafy CI/CD Pipeline

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build-and-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./locafy-microservices

steps:
# 1. Checkout Code
- name: Checkout code
uses: actions/checkout@v3

# 2. Setup Java 17
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: maven

# 3. Compile and Package with Maven
- name: Build with Maven
run: mvn clean package -DskipTests

# 4. Build Docker Images (CI Step)
- name: Build Docker Images
run: docker compose build

# 5. Deploy to Runner Environment (Test Deployment)
- name: Start Services
run: docker compose up -d

# 6. Verify Services are Running
- name: Check Running Containers
run: |
sleep 30 # Give services time to start
docker compose ps
docker logs notification-service
File renamed without changes.
125 changes: 125 additions & 0 deletions ReadMe5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Locafy - Microservices Architecture - EXTENDED

Locafy is a local business discovery platform implemented using Spring Boot Microservices.

## Architecture

1. **Discovery Server (Eureka):** Service Registry.
2. **API Gateway:** Routes all traffic (Port 8080).
3. **Business Service:** Manages business profiles (Uses Builder & Strategy patterns).
4. **Review Service:** Manages reviews. Validates business existence via REST calls to Business Service.
5. **Notification Service:** Simulates sending alerts when reviews are posted.


Part 1: Architecture Upgrade (Synchronous vs. Asynchronous)
Current State (Synchronous): Review Service -> calls Notification Service directly via HTTP (Feign).

Problem: If the Notification Service is down, the Review Service fails or hangs. The user has to wait for the email to be sent before their review is saved.

New State (Asynchronous with RabbitMQ): Review Service -> sends a message to RabbitMQ -> Notification Service picks it up later.

Benefit: Decoupling. The Review Service doesn't care if the Notification Service is online. It saves the review, sends a message to the queue, and responds to the user immediately (Scalability & Fault Tolerance).


The Change
services:
# ... (Discovery, Gateway, Business Service remain the same) ...

# NEW: Message Broker
rabbitmq:
image: rabbitmq:3.12-management
container_name: rabbitmq
ports:
- "5672:5672" # App communication port
- "15672:15672" # Management Dashboard (http://localhost:15672)
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest

review-service:
build: ./review-service
container_name: review-service
ports:
- "8082:8082"
environment:
- EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://discovery-server:8761/eureka/
- SPRING_RABBITMQ_HOST=rabbitmq # Connect to Rabbit container
depends_on:
- discovery-server
- business-service
- rabbitmq # Wait for RabbitMQ


adding this to the review-service pom: https://spring.io/projects/spring-amqp
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

adding this to: src/main/java/com/locafy/review/config/RabbitMQConfig.java

package com.locafy.review.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {
public static final String QUEUE = "notification.queue";
public static final String EXCHANGE = "review.exchange";
public static final String ROUTING_KEY = "review.routingKey";

@Bean
public Queue queue() { return new Queue(QUEUE); }

@Bean
public TopicExchange exchange() { return new TopicExchange(EXCHANGE); }

@Bean
public Binding binding(Queue queue, TopicExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY);
}
}

and then
Update Service Logic: Modify ReviewService.java to use RabbitTemplate instead of NotificationClient.





old code to be added back
package com.example.review_service;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ReviewServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ReviewServiceApplication.class, args);
}
}



Use this for Docker and not the intelij plugin: docker compose up --build



# 1. Ensure fresh JARs are available
mvn clean package -DskipTests

# 2. Build and start containers using the V2 plugin
docker compose up --build


Step 4: CI/CD Pipeline (GitHub Actions)
This pipeline will run every time you push code to GitHub. It will verify that your Java code compiles and that your Docker containers can build and start up successfully.

Create this file structure in your project root: .github/workflows/maven-docker.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,17 @@ services:
depends_on:
- discovery-server

notification-service:
build: ./notification-service
container_name: notification-service

# EXTENDED MICROSERV. with RabbitMQ
rabbitmq:
image: rabbitmq:3.12-management
container_name: rabbitmq
ports:
- "8083:8083"
- "5672:5672" # App communication port
- "15672:15672" # Management dashboard foir RabbitMQ (http://localhost:15672)
environment:
- EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://discovery-server:8761/eureka/
depends_on:
- discovery-server
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest

review-service:
build: ./review-service
Expand All @@ -47,6 +49,21 @@ services:
- "8082:8082"
environment:
- EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://discovery-server:8761/eureka/
- SPRING_RABBITMQ_HOST=rabbitmq # Connect to Rabbit container
depends_on:
- discovery-server
- business-service
- business-service
- rabbitmq # Wait for RabbitMQ

notification-service:
build: ./notification-service
container_name: notification-service
ports:
- "8083:8083"
environment:
- EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://discovery-server:8761/eureka/
- SPRING_RABBITMQ_HOST=rabbitmq
depends_on:
- discovery-server
- rabbitmq

Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example.notification_service.consumer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class NotificationConsumer {

private static final Logger log = LoggerFactory.getLogger(NotificationConsumer.class);

// Listens to the queue defined in the Review Service
@RabbitListener(queues = "notification.queue")
public void consumeMessage(String message) {
log.info("------------------------------------------------");
log.info("🐰 RABBITMQ MESSAGE RECEIVED: {}", message);
log.info("------------------------------------------------");
// Here you would add logic to send actual Email/SMS
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>

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

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.example.review_service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;

//@SpringBootApplication
//@EnableDiscoveryClient
//@EnableFeignClients
//@EnableScheduling // <--- CRITICAL: Enables the timer
//public class ReviewServiceApplication {
//
// private static final Logger log =
// LoggerFactory.getLogger(ReviewServiceApplication.class);
//
// public static void main(String[] args) {
// SpringApplication.run(ReviewServiceApplication.class, args);
// log.info("🚀 APPLICATION STARTED");
// }
//
// @Scheduled(fixedRate = 3000)
// public void heartbeat() {
// log.info("💓 HEARTBEAT – app is alive");
// }
//}

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ReviewServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ReviewServiceApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.example.review_service.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class RabbitMQConfig {
public static String QUEUE = "notification.queue";
public static String EXCHANGE = "review.exchange";
public static String ROUTING_KEY = "review.routingKey";

@Bean
public Queue queue() { return new Queue(QUEUE); } //this is not the queue of java.util.queue but
//but it is org.springframework.amqp.core.Queue;

@Bean
public TopicExchange exchange() { return new TopicExchange(EXCHANGE); }

@Bean
public Binding binding(Queue queue, TopicExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.example.review_service.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(RuntimeException.class)
public ResponseEntity<Map<String, String>> handleRuntimeException(RuntimeException ex) {
Map<String, String> response = new HashMap<>();
response.put("error", "Validation Error");
response.put("message", ex.getMessage());
response.put("status", "404");

// Return 404 Not Found instead of 500 Internal Server Error
return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
}
}
Loading
Loading