Microservices Architecture & Design

Core Microservice Principles

18 min Lesson 2 of 12

Core Microservice Principles

Microservices is not a technology — it is a set of architectural principles. You can build a microservices system with any language and any framework, but without applying the underlying principles you end up with a distributed monolith: all the operational complexity of distributed systems with none of the independence benefits. This lesson examines the two most fundamental principles — single responsibility and autonomous, independently deployable services — and shows what they mean concretely in a Spring Boot 3 codebase.

Principle 1: Single Responsibility

The Single Responsibility Principle (SRP) is not new — Robert C. Martin formulated it for classes in the 1990s. In microservices architecture it scales up: each service should have exactly one reason to change. That reason is usually aligned with a single business capability.

Consider an e-commerce platform. A naive split might be "frontend service" and "backend service." That is not responsibility-aligned; both still change whenever any business requirement changes. A responsibility-aligned split looks like:

  • Order Service — owns the lifecycle of an order (placed, confirmed, shipped, cancelled).
  • Inventory Service — owns stock levels, reservation, and replenishment.
  • Notification Service — owns how and when customers are contacted (email, SMS, push).
  • Pricing Service — owns price rules, discounts, and promotions.

With this structure, adding a new promotion type means touching only the Pricing Service. Changing the email provider means touching only the Notification Service. Neither change threatens the other services.

The SRP test: If a developer can describe what a service does using the word "and" — "it handles orders and sends emails and manages stock" — the service violates SRP and should be split.

SRP in Practice: A Spring Boot Order Service

A correctly scoped Order Service exposes only order-centric operations. Its @RestController handles order commands and queries; it does not contain email logic or stock decrement logic. Those are published as events for other services to react to.

// order-service/src/main/java/com/example/order/OrderController.java @RestController @RequestMapping("/api/orders") @RequiredArgsConstructor public class OrderController { private final OrderService orderService; @PostMapping @ResponseStatus(HttpStatus.CREATED) public OrderResponse placeOrder(@Valid @RequestBody PlaceOrderRequest request) { return orderService.place(request); } @GetMapping("/{id}") public OrderResponse getOrder(@PathVariable Long id) { return orderService.findById(id); } @PatchMapping("/{id}/cancel") public OrderResponse cancelOrder(@PathVariable Long id) { return orderService.cancel(id); } }

Notice what is absent: no EmailSender, no StockRepository, no payment gateway call. The service emits a domain event after state changes, and downstream services handle the rest.

// Within OrderService.place(): Order saved = orderRepository.save(order); // Publish event — Notification and Inventory services consume this independently applicationEventPublisher.publishEvent(new OrderPlacedEvent(saved.getId(), saved.getCustomerId())); return toResponse(saved);

Principle 2: Autonomy and Independent Deployability

The second core principle is that each microservice must be deployable on its own schedule, without coordinating with other services. This is what makes it possible for a team of four to deploy ten times per day without needing a company-wide release window.

Autonomy has several concrete requirements:

  • Own data store: the service has its own database schema (or database instance). No other service can reach into it via shared tables — communication is always through the API or event stream.
  • Own build and deploy pipeline: the service has its own Dockerfile, its own CI/CD pipeline, and its own Kubernetes deployment manifest.
  • Backward-compatible API changes: adding a field to a response is safe; removing or renaming one breaks consumers. Autonomous services evolve their APIs carefully (consumer-driven contract testing helps).
  • Runtime isolation: a crash in the Notification Service must not cascade into the Order Service. Circuit breakers (Resilience4j) and timeouts enforce this boundary.
Conway\'s Law alignment: Autonomous services work best when team boundaries match service boundaries. If the same team owns five services, they can move fast. If five teams share one service, every deployment needs coordination — you are back to a monolith in practice.

Independent Deployability in a Spring Boot Service

A Spring Boot application bundles everything it needs into a single executable JAR (the "fat JAR" or "uber JAR"). This is what makes independent deployment practical.

# order-service/Dockerfile FROM eclipse-temurin:21-jre-alpine WORKDIR /app COPY target/order-service-*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]

The application.yml of each service contains only its own configuration. It does not share a global config file with other services (though it may pull values from a Config Server — each service still reads its own section).

# order-service/src/main/resources/application.yml spring: application: name: order-service datasource: url: ${DB_URL:jdbc:postgresql://localhost:5432/orders} username: ${DB_USER:orders_user} password: ${DB_PASS:changeme} jpa: hibernate: ddl-auto: validate server: port: 8080 management: endpoints: web: exposure: include: health,info,metrics

Security Implications of Autonomy

When services are autonomous, each one is its own security boundary. This has direct consequences:

  • Authentication is distributed: every service must validate tokens independently. In Spring Security 6 each service is configured as an OAuth 2.0 Resource Server validating JWTs against the Authorization Server's JWKS endpoint.
  • Secrets per service: each service owns its own database credentials, API keys, and TLS certificates. A compromise of one service should not expose another service's credentials.
  • Principle of least privilege applies at the network level: the Order Service should not have network access to the Notification Service's database. Kubernetes NetworkPolicies or a service mesh enforce this.
// Each service independently configures Spring Security as a Resource Server @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/actuator/health").permitAll() .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt.jwkSetUri("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")) ); return http.build(); } }

Distributed-Systems Trade-offs

Single responsibility and autonomy are not free. Every split introduces network hops where function calls used to suffice. A few realities every working developer must accept:

  • Network calls can fail. A direct database join that was microseconds now involves an HTTP call that might time out. You must design for partial failure from day one.
  • Consistency is harder. A monolith can wrap two operations in a single database transaction. Two autonomous services cannot — you need sagas, outbox patterns, or eventual consistency (covered in later lessons).
  • Operational overhead is real. Running ten services means ten sets of logs, ten dashboards, ten sets of deployment manifests. Invest in observability (distributed tracing, structured logging, metrics) early.
Do not split prematurely. Amazon, Netflix, and Uber did not start with hundreds of microservices. They started with monoliths and extracted services along proven seam lines. Splitting too early, before the domain boundaries are well understood, produces services that are tightly coupled to each other — the worst of both worlds.

Summary

The two core principles — single responsibility and autonomous, independently deployable services — are the foundation every other microservices pattern builds upon. Single responsibility keeps services small and changesets contained. Autonomy keeps teams independent and deployment risk low. In Spring Boot 3 these principles manifest as focused controllers, domain-event publishing, per-service databases and configuration, standalone Dockerfiles, and per-service Spring Security configuration. The next lesson moves from principles to boundaries: how to identify where one service ends and another begins using Domain-Driven Design's concept of Bounded Contexts.