diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cafb874 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + java: ["17"] + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK ${{ matrix.java }} + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ matrix.java }} + cache: maven + + - name: Build and run tests + run: mvn -B -DskipTests=false clean verify + + - name: Build app Docker image + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + run: | + docker build -t java-microservices-app:latest ./app + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb60b3e --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Maven +**/target/ +**/release/ + +# Eclipse / IntelliJ +.classpath +.project +.settings/ +*.iml +.idea/ + +# OS files +.DS_Store +Thumbs.db + +# Logs +*.log + +# Maven wrapper +.mvn/wrapper/maven-wrapper.jar + +# local maven repo +.m2/ + +# Docker +docker-compose.override.yml + +# VS Code +.vscode/ + diff --git a/README.md b/README.md index eb2fd1f..22a1ed4 100644 --- a/README.md +++ b/README.md @@ -1 +1,120 @@ # Java Microservices + +This repository is a blueprint and starter kit for building high-performance, production-ready Java backend systems that can be implemented either as a performant monolith or as a scalable set of microservices. + +## Goals +- Provide a pragmatic, senior-backend-developer-approved architecture and tooling selection. +- Show recommended technology choices (Spring Boot + Quarkus) and cross-cutting tools for DB, messaging, caching, CI/CD, observability and security. +- Offer an incremental path: start with a modular high-performance monolith and split to microservices where it makes sense. + +## High-level vision +- Keep core business logic domain-driven and framework-agnostic. +- Design for observability, security, and automation from day one. +- Use modern, proven tools to enable fast developer feedback and safe production deployments. + +## Key non-functional requirements +- Performance: low latency (sub-100ms P95 for typical API calls), high throughput. +- Scalability: horizontal scalability for stateless services, and appropriate patterns for stateful services. +- Reliability: graceful degradation, retries, bulkheads, and circuit breakers. +- Maintainability: small modules/contexts, strong typing, and automated tests. +- Observability: structured logs, distributed tracing, metrics, and alerting. + +## Recommended tech stack +- JVM & language: Java 17 or 21 (LTS), use modern language features where helpful. +- Frameworks: + - Spring Boot 3.x for full-featured, ecosystem-rich services (REST, Spring Data, Spring Security). + - Quarkus for performance-sensitive services (fast startup, low memory) and native compilation where needed. +- Build tools: Maven or Gradle (pick one consistently). Provide sample Maven setup by default. +- Database: PostgreSQL (primary OLTP store). Use R2DBC for reactive services where appropriate; otherwise use JDBC via Spring Data JPA. +- Migrations: Flyway (or Liquibase) for database schema/version control. +- Caching: Redis (for distributed caches) or Hazelcast (in-memory grid) when needed. +- Messaging / events: Apache Kafka (event-driven, durable streaming) and RabbitMQ (if you need simpler broker semantics). +- API protocols: REST (OpenAPI), gRPC for high-performance polyglot RPC, GraphQL optional for rich client queries. +- Security: OAuth 2.0 / OIDC (Keycloak or Auth0), JWT tokens, Spring Security, and Vault for secrets. +- Observability: OpenTelemetry (traces + metrics), Jaeger/Tempo for tracing, Prometheus for metrics, Grafana for dashboards. +- Testing: JUnit 5, Mockito, Testcontainers (for integration tests using real Postgres/Kafka), Pact for contract testing. +- Performance testing: Gatling or k6 for load tests; JMH for microbenchmarks. +- Containerization / orchestration: Docker, Kubernetes (K8s), Helm charts; optionally use Kustomize or ArgoCD for GitOps. +- CI/CD: GitHub Actions or GitLab CI for pipelines, with stages for build, test, security scans, container publish, and deploy. +- Secrets & Config: HashiCorp Vault for secrets, Spring Cloud Config / Consul or environment-driven 12-factor config for app configuration. + +## Architecture patterns & design principles +- Start as a modular monolith (multi-module Gradle/Maven project) organized by bounded contexts. This gives fast developer feedback and avoids premature distributed systems complexity. +- Apply Hexagonal / Ports & Adapters architecture to keep business logic independent from frameworks and infrastructure. +- Use Domain-Driven Design (DDD) to identify bounded contexts and where to split into microservices. +- Prefer asynchronous messaging and event-driven integration for inter-service communication when eventual consistency is acceptable. +- Use API Gateway for external APIs (rate-limiting, authentication, routing). Keep internal APIs lightweight. +- Use health checks (liveness/readiness), graceful shutdown, and resource limits for containers. + +## Project layout suggestions +- Monolith (modular): + - /app (service application starters) - Spring Boot / Quarkus launchers + - /domain (shared domain model & services) + - /infrastructure (db, messaging, cache adapters) + - /api (REST controllers / gRPC endpoints) + - /integration-tests (Testcontainers-based tests) +- Microservices: + - service-name/ (each service: own module/repo, own Dockerfile, Helm chart) + - shared-libs/ (common libraries maintained with versioning) + +## Implementation contract (short) +- Inputs: HTTP/gRPC requests, async messages, DB events. +- Outputs: HTTP/gRPC responses, domain events, DB writes, metrics, logs, traces. +- Error modes: transient infra failures (handled by retries & backoff), validation errors (client 4xx), auth/permission (401/403). +- Success criteria: automated build + tests, deployable container image, passing health checks, and basic load test within target SLOs. + +## Developer UX & local dev setup +- Provide a docker-compose file to bring up Postgres, Kafka (or RabbitMQ), Redis, and Keycloak for local development. +- Use dev profiles (Spring profiles or Quarkus config) to switch between in-memory/mocked dependencies and real infra. +- Prefer devtools / hot-reload (Spring DevTools, Quarkus dev mode) for fast feedback. + +## CI/CD pipeline outline +1. Checkout code and run static analysis (spotbugs, checkstyle, dependency-check). +2. Build with Maven/Gradle and run unit tests. +3. Run integration tests using Testcontainers (or a test environment). +4. Build Docker image and run a lightweight smoke test. +5. Push image to registry and create an immutable version/tag. +6. Deploy to staging via Helm or GitOps, run end-to-end and contract tests. +7. Promote to production with a controlled rollout (canary, blue/green). + +## Security checklist +- Enforce HTTPS everywhere (nginx/ALB + service TLS). +- Use OAuth2/OIDC for authentication; never roll your own auth. +- Validate and sanitize inputs; use parameterized queries or ORM to prevent SQL injection. +- Short-lived JWTs + refresh tokens; rotate secrets stored in Vault. +- Apply principle of least privilege for service accounts and database credentials. +- Use static analysis and dependency vulnerability scanning (Snyk, Dependabot, or OWASP Dependency-Check) in the pipeline. + +## Observability & SLOs +- Capture structured logs (JSON) with request IDs. +- Export traces and metrics via OpenTelemetry libraries to Jaeger and Prometheus. +- Define SLOs (error rates, latency P95/P99) and set up Grafana dashboards and alerts. + +## Testing strategy +- Unit tests for business logic (JUnit 5 + Mockito), aim for fast execution. +- Integration tests using Testcontainers to run Postgres/Kafka in CI for realistic integration. +- Contract tests (Consumer-Driven Contracts) to protect service boundaries. +- End-to-end smoke tests after deployment to staging. +- Load testing in staging with real-ish data using Gatling/k6. + +## Performance tips +- Prefer connection pooling (HikariCP), efficient query patterns, and proper indexing for Postgres. +- For high concurrency paths, consider reactive stacks (R2DBC, WebFlux, Mutiny in Quarkus) and benchmark carefully. +- Use caching for read-heavy endpoints; measure cache hit ratio and TTLs. +- Profile hot paths using async-profiler / JFR and iterate. + +## Minimal MVP (first milestone) +1. Core domain module with a single bounded context (e.g., Orders, Users, Inventory) implemented in a modular monolith. +2. REST API with OpenAPI docs and basic CRUD flows. +3. Postgres persistence with Flyway-managed schema. +4. Dockerfile and docker-compose for local dev (Postgres + Redis + Keycloak minimal). +5. CI pipeline with build, unit tests and a basic integration stage. +6. Logging, health endpoints, and a Prometheus metrics endpoint. + +## Roadmap & next steps +- Phase 1: Create modular monolith with domain-first design + baseline CI and infra (DB, cache). +- Phase 2: Add observability (traces, metrics, dashboards) and security (Keycloak integration). +- Phase 3: Introduce asynchronous messaging and eventing for selected flows. +- Phase 4: Split off the first microservice from monolith (bounded context extraction) and deploy to Kubernetes. +- Phase 5: Harden CI/CD, rollout strategies, and secrets management. + diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..0a4be5b --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,12 @@ +# Multi-stage Dockerfile +FROM maven:3.9.4-eclipse-temurin-17 AS build +WORKDIR /workspace +COPY . /workspace +RUN mvn -B -DskipTests package -pl app -am + +FROM eclipse-temurin:17-jre-alpine +WORKDIR /app +COPY --from=build /workspace/app/target/*.jar /app/app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "/app/app.jar"] + diff --git a/app/pom.xml b/app/pom.xml new file mode 100644 index 0000000..154555f --- /dev/null +++ b/app/pom.xml @@ -0,0 +1,55 @@ + + 4.0.0 + + + com.example + java-microservices + 0.0.1-SNAPSHOT + + + app + jar + + app + + + ${spring-boot.version} + + + + + org.springframework.boot + spring-boot-starter-web + + + + com.example + core + 0.0.1-SNAPSHOT + + + + com.example + common + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/app/src/main/java/com/example/app/Application.java b/app/src/main/java/com/example/app/Application.java new file mode 100644 index 0000000..b73aa59 --- /dev/null +++ b/app/src/main/java/com/example/app/Application.java @@ -0,0 +1,12 @@ +package com.example.app; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication(scanBasePackages = "com.example") +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} + diff --git a/app/src/main/java/com/example/app/controller/GreetingController.java b/app/src/main/java/com/example/app/controller/GreetingController.java new file mode 100644 index 0000000..95cef1f --- /dev/null +++ b/app/src/main/java/com/example/app/controller/GreetingController.java @@ -0,0 +1,24 @@ +package com.example.app.controller; + +import com.example.common.dto.GreetingDto; +import com.example.core.service.GreetingService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class GreetingController { + + private final GreetingService greetingService; + + public GreetingController(GreetingService greetingService) { + this.greetingService = greetingService; + } + + @GetMapping("/api/greet") + public GreetingDto greet(@RequestParam(required = false) String name) { + String message = greetingService.greet(name); + return new GreetingDto(name, message); + } +} + diff --git a/app/src/main/resources/application.yml b/app/src/main/resources/application.yml new file mode 100644 index 0000000..ea52ccf --- /dev/null +++ b/app/src/main/resources/application.yml @@ -0,0 +1,7 @@ +server: + port: 8080 + +spring: + main: + allow-bean-definition-overriding: false + diff --git a/app/src/test/java/com/example/app/GreetingControllerTest.java b/app/src/test/java/com/example/app/GreetingControllerTest.java new file mode 100644 index 0000000..e35033f --- /dev/null +++ b/app/src/test/java/com/example/app/GreetingControllerTest.java @@ -0,0 +1,28 @@ +package com.example.app; + +import com.example.common.dto.GreetingDto; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.server.LocalServerPort; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class GreetingControllerTest { + + @LocalServerPort + private int port; + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void greetEndpoint() { + GreetingDto resp = this.restTemplate.getForObject("http://localhost:" + port + "/api/greet?name=Bob", GreetingDto.class); + assertThat(resp).isNotNull(); + assertThat(resp.getMessage()).isEqualTo("Hello, Bob"); + } +} + diff --git a/common/pom.xml b/common/pom.xml new file mode 100644 index 0000000..370e68f --- /dev/null +++ b/common/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + + com.example + java-microservices + 0.0.1-SNAPSHOT + + + common + jar + + common + + + + + com.fasterxml.jackson.core + jackson-databind + 2.15.2 + compile + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + + + + diff --git a/common/src/main/java/com/example/common/dto/GreetingDto.java b/common/src/main/java/com/example/common/dto/GreetingDto.java new file mode 100644 index 0000000..f9fd2af --- /dev/null +++ b/common/src/main/java/com/example/common/dto/GreetingDto.java @@ -0,0 +1,31 @@ +package com.example.common.dto; + +public class GreetingDto { + private String name; + private String message; + + public GreetingDto() { + } + + public GreetingDto(String name, String message) { + this.name = name; + this.message = message; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} + diff --git a/common/src/test/java/com/example/common/dto/GreetingDtoTest.java b/common/src/test/java/com/example/common/dto/GreetingDtoTest.java new file mode 100644 index 0000000..97490ca --- /dev/null +++ b/common/src/test/java/com/example/common/dto/GreetingDtoTest.java @@ -0,0 +1,16 @@ +package com.example.common.dto; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class GreetingDtoTest { + + @Test + void basicPojo() { + GreetingDto dto = new GreetingDto("Alice", "Hello Alice"); + assertEquals("Alice", dto.getName()); + assertEquals("Hello Alice", dto.getMessage()); + } +} + diff --git a/core/pom.xml b/core/pom.xml new file mode 100644 index 0000000..08ed75d --- /dev/null +++ b/core/pom.xml @@ -0,0 +1,37 @@ + + 4.0.0 + + + com.example + java-microservices + 0.0.1-SNAPSHOT + + + core + jar + + core + + + + com.example + common + 0.0.1-SNAPSHOT + + + + org.springframework + spring-context + + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + + + + diff --git a/core/src/main/java/com/example/core/service/GreetingService.java b/core/src/main/java/com/example/core/service/GreetingService.java new file mode 100644 index 0000000..af98dc3 --- /dev/null +++ b/core/src/main/java/com/example/core/service/GreetingService.java @@ -0,0 +1,6 @@ +package com.example.core.service; + +public interface GreetingService { + String greet(String name); +} + diff --git a/core/src/main/java/com/example/core/service/impl/GreetingServiceImpl.java b/core/src/main/java/com/example/core/service/impl/GreetingServiceImpl.java new file mode 100644 index 0000000..f48bb85 --- /dev/null +++ b/core/src/main/java/com/example/core/service/impl/GreetingServiceImpl.java @@ -0,0 +1,17 @@ +package com.example.core.service.impl; + +import com.example.core.service.GreetingService; +import org.springframework.stereotype.Service; + +@Service +public class GreetingServiceImpl implements GreetingService { + + @Override + public String greet(String name) { + if (name == null || name.isBlank()) { + return "Hello, World"; + } + return "Hello, " + name.trim(); + } +} + diff --git a/core/src/test/java/com/example/core/GreetingServiceTest.java b/core/src/test/java/com/example/core/GreetingServiceTest.java new file mode 100644 index 0000000..75b3e48 --- /dev/null +++ b/core/src/test/java/com/example/core/GreetingServiceTest.java @@ -0,0 +1,23 @@ +package com.example.core; + +import com.example.core.service.GreetingService; +import com.example.core.service.impl.GreetingServiceImpl; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class GreetingServiceTest { + + @Test + void greetWithName() { + GreetingService svc = new GreetingServiceImpl(); + assertEquals("Hello, Alice", svc.greet("Alice")); + } + + @Test + void greetWithNull() { + GreetingService svc = new GreetingServiceImpl(); + assertEquals("Hello, World", svc.greet(null)); + } +} + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..55ca8cf --- /dev/null +++ b/pom.xml @@ -0,0 +1,55 @@ + + 4.0.0 + + com.example + java-microservices + 0.0.1-SNAPSHOT + pom + + java-microservices + Starter multi-module project for Java microservices (Spring Boot) + + + 17 + 3.2.0 + 3.10.1 + UTF-8 + + + + common + core + app + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven.compiler.plugin.version} + + ${java.version} + ${java.version} + + + + + + +