6.2 Microservice Architecture Methodology
6.2.1 Microservice Architecture Principles and Their Adaptation to IoT Scenarios
The preceding sections discussed how to write a single service and how it sends and receives data, but a real IoT system is far more than one service. Hundreds of thousands of devices reporting data at the same time, alarm evaluation completed within seconds, multi-tenancy and dynamic scaling — at this scale, a monolithic application runs into bottlenecks one after another. The microservice architecture is precisely the core methodology for problems of scale like these. IoT scenarios, however, have their own particularities: a wide variety of devices, high data throughput, and links that are extremely sensitive to latency. Copying internet microservice design patterns wholesale tends to land teams in pitfalls. This section first lays out the core principles of microservices, then analyzes the adaptation challenges in IoT scenarios and the approaches to meeting them.
Service Decomposition: Where Microservices Start
The core idea of the microservice architecture is to split a large system into multiple small services, each built, deployed, and evolved independently around a specific business capability. The idea itself is not a new invention, but only after container technology and cloud-native infrastructure matured did it truly land in large-scale engineering practice. The following principles help judge whether a decomposition boundary is sound:
- Single responsibility: each service is responsible for one thing, and does it well. In an IoT platform, "device registration" and "data storage" are different responsibilities and belong in different services.
- Service autonomy: each service owns its own database and runtime environment and does not directly depend on other services' internal data. Services communicate only through defined APIs.
- Decentralization: there is no unified "god service" controlling everything. Teams can choose technology stacks independently — one service written in Java, another in Python, as long as they follow the same interface contracts.
- Independent deployment: modifying one service does not require redeploying the entire system. This is especially critical in IoT scenarios — a bug fix in one protocol driver must not affect the operation of other drivers.
- Fault tolerance: one service going down must not drag the entire system down with it. Failures are isolated through mechanisms such as circuit breaking, degradation, and retries.
These principles directly shape how modules are divided. Systems are typically decomposed by domain: the gateway service, device management service, data service, and alarm service each run independently and maintain their own data. If a protocol driver (a Modbus driver, for example) develops a memory leak, it affects only that driver module, not the entire platform.
The Challenges IoT Scenarios Pose to Microservices
Applying microservice principles to IoT systems runs into several practical obstacles.
Challenge one: the complexity of protocol adaptation brought by device diversity. An IoT platform may need to accept MQTT, Modbus, OPC UA, CoAP, and other protocols at the same time. The access logic of each protocol differs greatly, yet at the business layer they all look like "device data." Splitting services one-size-fits-all by "protocol type" creates heavy code duplication; not splitting them couples all the protocols inside one service. The reasonable approach is the adapter pattern at the collection layer — each protocol driver is an independent microservice, but all of them expose a unified device abstraction interface to the layers above. This preserves the independence of protocol adaptation while keeping data formats consistent. A common practice in the industrial field is to provide multiple driver modules, each responsible for device access over one protocol, so upper-layer business services never need to care about the underlying protocol details.
Challenge two: massive data volume and real-time requirements. Example: a large number of temperature sensors report data at a fairly high frequency; after multiple service calls, serialization, and network transmission before reaching the storage layer, latency and throughput become unbearable. The solution is to divide the data flow into a "real-time hot path" and a "batch cold path." On the hot path, device data goes through the simplest processing (filtering, format conversion) and is written directly into the time-series database, bypassing business services along the way. On the cold path, the data is then aggregated, cleaned, and analyzed. In a common architecture, the data received by the collection service is written directly into a message queue, and the data service and alarm service consume from the queue instead of making synchronous HTTP calls.
Challenge three: coordinating edge computing with cloud microservices. IoT network conditions are unstable, and not every device can reach the cloud platform at all times. Some processing must happen where the device is located — the edge node — for example alarm evaluation, local caching, and reconnection after network loss. This raises an architectural question: is the edge node's functionality a subset of the cloud microservices, or a completely independent system? One common approach is "independent yet unified": each edge node runs a stripped-down set of microservices internally but stays synchronized with the cloud through a unified data model and API definitions. The Facade pattern supports this switching — in distributed deployment, the services communicate over gRPC or a message queue; in in-process mode (on a resource-constrained edge node, for instance), the same services can be packaged and run together without major code changes.
Domain-Driven Decomposition
"Split by function" sounds simple, but what exactly should become a service? A common trap is splitting by technical layer: a frontend service, a backend service, a database service — this merely breaks the monolith's three tiers into three microservices without achieving real separation of responsibilities. A more effective approach uses the Bounded Context concept from Domain-Driven Design (DDD): each business domain gets a clearly drawn boundary, cohesion stays high inside, and boundaries are decoupled from each other through events or APIs.
Take a smart building system as an example — several core domains can be identified:
- Device management: handles device registration, authentication, and configuration delivery.
- Data collection: receives raw data from devices, standardizes the format, and stores it in the time-series database.
- Alarm engine: evaluates rules to determine whether data triggers an alarm, generates alarm records, and notifies the people concerned.
- Energy analysis: aggregates historical data, computes energy-consumption trends, and generates reports.
- Users and tenants: handles user registration, permission assignment, and multi-tenant isolation.
Figure 6-3 shows the smart building microservice architecture after decomposition along DDD bounded contexts. Each domain also has different data storage needs: device management uses a relational database, data collection uses a time-series database, the alarm engine uses an in-memory database for fast evaluation, and energy analysis uses a data warehouse for aggregation queries.
In the figure, the protocol driver layer runs on the edge gateway and the business services run in the cloud. The two communicate through a message queue rather than HTTP — because the edge-to-cloud link can be unstable, and asynchronous messaging tolerates network jitter better. The gateway layer uniformly exposes REST APIs and WebSocket to the outside; clients do not call microservices directly.
Engineering Trade-offs: When Not to Split
Microservices are good, but every split has a cost: operations complexity rises, network latency grows, and data consistency becomes harder to guarantee. For an IoT project, the following situations warrant questioning whether decomposition is truly needed:
- Small device access volume: a monolithic application with sensible layering still suffices, and splitting into microservices only adds deployment and debugging cost.
- Small team: maintaining the build, test, and deployment pipelines of multiple microservices consumes a great deal of development time.
- Extremely stringent real-time requirements (sub-millisecond): the latency introduced by inter-service network calls is unacceptable. At this point consider edge computing or coroutine-level concurrency rather than distributed services.
A sound strategy is to start with a modular monolith and peel services out into independent units step by step once the real bottlenecks have been identified. This is not compromise; it is pragmatism. The microservice architecture ultimately serves business flexibility, not the other way around.
Examples of how to integrate AI capabilities (intelligent alarming, predictive maintenance) into a microservice architecture will be developed in later chapters. The next section discusses the concrete evolution path from monolith to microservices and the engineering risks each step may encounter.
6.2.2 From Monolith to Microservices: The Evolution Path of IoT Systems
The previous subsection discussed decomposition principles for microservices, but back on the engineering floor, few teams can stand up a complete microservice cluster on day one. Blurred business boundaries, unstable device protocols, insufficient headcount — these constraints dictate a more pragmatic path: start with a simple monolithic application, and peel services out step by step only when business pressure and team growth force the split. From the engineering field, a common evolution path looks roughly like this.
Suppose you are building a building energy-consumption monitoring system. Early on it manages only a small number of collection points, and the requirements are simple: collect data, generate reports, and occasionally deliver on/off commands. A monolithic application (Java + Spring Boot) plus a single-machine database easily carries all the functionality. Devices report data through an MQTT broker; a backend script consumes it, stores it, and triggers alarms, with frontend and backend running in the same process. At this stage almost no distributed-systems knowledge is required.
Stage one: monolithic prototype. All the code goes into one deployment unit, with a modular package structure dividing the internal responsibilities: com.example.energy.collector handles data collection, com.example.energy.alarm handles alarm processing, and com.example.energy.web handles the frontend console. The goal is to validate the business loop quickly, and the team usually numbers no more than three. The biggest advantage at this stage is development efficiency — change one line of alarm logging code, and build, deploy, and test all complete on one machine. When the number of collection points grows to several hundred, strain starts to show: alarm computation and data ingestion contend for CPU, occasional response times jump from a few hundred milliseconds to several seconds, and each new release takes correspondingly longer to deploy.
Stage two: peeling off a core module. As more device types come online (electricity meters, water meters, temperature-humidity sensors) and the data reporting volume grows, the alarm processing module demands real-time performance (second-level evaluation) while the data storage module demands write throughput (batch persistence). Two such different performance profiles are difficult for a monolith to serve at once. The team chooses to split out the "alarm processing" module first, because its logic is self-contained — it does not depend on the device registry and only reads point values. The peeling process has three steps: boundary identification (which tables the module operates on, which services it depends on), data isolation (migrating alarm-related tables to a separate database), and independent deployment (packaging the alarm service in a container and interacting with the main application over HTTP interfaces). Interface stability should be observed for at least two iteration cycles before deciding whether to peel off the next module. Within those two iteration cycles, if the new service shows timeouts or data inconsistency, the team can first roll back to the monolithic version.
Stage three: event-driven rework. The device access module hits its ceiling as well: when the monolith's API receives device data, protocol parsing, data writing, cache updates, and threshold evaluation all execute serially, and per-request latency worsens as concurrency rises. The team introduces an event-driven architecture — device messages are published through the MQTT broker to a message queue, and the consumers scale independently. After the rework, data collection and business processing are thoroughly decoupled. Even if one consumer is temporarily down, messages simply back up in the queue instead of causing device reporting failures in the field. Each consumer can auto-scale on resource utilization, no longer bounded by the resource limits of the monolithic process.
Stage four: continuous evolution. The project grows from a few buildings to dozens, and the team splits out a user management service, a device registration service, a historical data archival service, and more by business scenario. At the same time, modules with strong functional affinity (device registration and the device shadow, for example) are kept as an aggregated service, avoiding unnecessary distributed transactions. Evolution has no fixed endpoint; it is a structural decision continuously adjusted as the business grows. A different project may need entirely different split boundaries, but the monolith-to-microservices path itself is a common practice across the industry. Worth noting: device-count growth in IoT scenarios often arrives in stepped jumps (a new campus added, a batch of devices brought online) rather than the smooth growth of internet scenarios, so the window for splitting is narrower and the judgment between too early and too late is more sensitive.
Anti-patterns in Evolution
Anti-pattern one: splitting too early. With only a few dozen devices, the team splits into multiple microservices by function. Every change requires coordinating interface integration testing across different services, and development efficiency ends up lower than the monolith's. The telltale signal: the vast majority of interface calls are still direct in-process method calls that need no network communication at all. At this point there is only extra maintenance cost and no scalability gain.
Anti-pattern two: splitting too late. After the device count grows into the thousands, a single deployment of the monolith takes more than ten minutes, and every version update involves a full restart. A bug fix in the alarm module blocks new features from going live in the device access module; the team exceeds ten people and code conflicts flare up frequently. Splitting at this point is extremely costly: splitting database tables, migrating historical data, re-wiring interfaces, realigning business rules — every step can affect live devices.
Anti-pattern three: introducing distributed transactions immediately after the split. The moment the split happens, someone reaches for two-phase commit to guarantee strong data consistency. In IoT scenarios many business flows tolerate eventual consistency (device status updates, for example), and introducing strong-consistency locks actually lowers availability. The better approach is to manage failure rollback with a compensation mechanism (Saga) first, and evaluate whether strong consistency is needed only after the system has stabilized.
Engineering Decision Checklist
When facing an evolution decision, run through the following checklist for a quick judgment:
- Boundary identification: does the module own independent business entities and a data lifecycle of its own? If yes, it suits splitting. Device registration data and alarm rules, for instance, share no data coupling and are good candidates for separation.
- Team maturity: after the split, is there a clearly designated team responsible for maintenance? Do not split with insufficient headcount, or coordination costs grow. A small team splitting out six services, each maintained by half a person, carries extreme risk.
- Performance bottleneck: is the module the current system bottleneck? If yes, split it first; otherwise wait until a bottleneck appears. If the resource utilization curve fluctuates smoothly, the time to split has not yet come.
- Interface feasibility: can a clear interface contract be defined with REST/gRPC/message queues? If the interfaces churn frequently, the splitting cost is too high — consider an adapter layer first. An adapter layer can encapsulate unstable interfaces and reduce the direct dependencies between services.
- Deployment independence: can the module be deployed and rolled back independently? If not, the coupling is too strong and decoupling preparation is needed first. For example, before splitting a shared database table, decouple first through data views.
Risk analysis: under a step-by-step peeling strategy, reserve at least two iteration cycles after each split to verify interface stability and data consistency before deciding whether to peel off the next module. Before splitting, monitor the full set of metrics — interface call chains, database connection pools, network latency — to ensure that after the new service goes live, the system's overall performance is no worse than the original monolith's. The recommendation is to split only one module at a time and observe for a quarter before deciding the next move.
The core idea of this evolution path is that when to split matters more than how to split. A well-designed monolithic system, at the stage where scalability is lacking but the logic is clear, is far better than a microservice cluster chopped apart too early with chaotically coupled interfaces. For IoT projects, transitioning steadily from monolith to microservices is more reliable than getting there in one leap.
6.2.3 Service Discovery, Configuration Management, and API Gateway
Once the microservices are split out, three foundational questions appear immediately: how does service A find service B? How are configuration changes delivered to multiple instances? Where do external clients enter the system? They correspond respectively to service discovery, configuration management, and the API gateway. All three are general microservice capabilities, but that does not mean every project must deploy an independent registry.
Service Discovery: First Decide Whether a Registry Is Truly Needed
The goal of service discovery is to let a caller locate dynamic instances through a stable name. Different deployment forms already provide varying degrees of this foundation: Kubernetes can resolve services with Service objects and cluster DNS; Compose lets services reach each other by service name within the container network; only when there is cross-environment dynamic registration, frequently changing instances, or a need for unified health management is it necessary to evaluate independent components such as Nacos or Consul.
Table 6-3 illustrates generic selection dimensions; it does not represent IoT DC3's current component inventory.
Table 6-3 Common options for service discovery and configuration management compared
| Option | Service discovery approach | Configuration capability | Applicable boundary |
|---|---|---|---|
| Kubernetes | Service + cluster DNS | ConfigMap / Secret | Clusters already on Kubernetes |
| Compose | Stable service names + container DNS | Environment variables + YAML | Small-to-medium scale or single-cluster deployments |
| Nacos | Dynamic registration and health checks | Centralized configuration with push | Spring Cloud ecosystems with a genuine dynamic governance need |
| Consul | Dynamic registration and health checks | Key-Value configuration | Cross-language service discovery and infrastructure governance |
IoT DC3 currently does not introduce Nacos, Eureka, Consul, or ZooKeeper. Gateway routes and gRPC channels use fixed service names, the Compose network handles DNS resolution, and addresses can be overridden through environment variables such as CENTER_*_HOST and GATEWAY_ROUTE_*_URI. When a Driver starts and calls the Manager's gRPC interface, it performs driver business registration and metadata synchronization — not the registration of a network address with a service registry.
Configuration Management: Separate Centralized Governance from Environment Injection
Collection intervals, broker addresses, database connections, and route addresses are all configuration, but they do not change at the same frequency. Rules that need runtime dynamic pushing can go into a centralized configuration system; addresses, credentials, and ports bound to the deployment environment are better injected through environment variables or Secrets. Pushing all configuration into the same dynamic configuration center only widens the failure surface and the scope for operator error.
IoT DC3 currently keeps default configuration in the project YAML and overrides environment-specific parameters with environment variables at deployment time. This approach lacks Nacos-style dynamic refresh, but it matches the current Compose service scale and removes one more control-plane component that would need separate operations. Only when explicit needs emerge — multi-cluster configuration governance, dynamic canary releases, or large-scale instance changes — should introducing a configuration center be re-evaluated.
API Gateway: Current Routes Use Fixed Service Names
The API gateway uniformly handles authentication, routing, and the northbound interface boundary, preventing clients from directly accessing the center services. IoT DC3 uses Spring Cloud Gateway; route targets are fixed service names in the container network and can be overridden by environment variables. For example, the actual configuration pattern of the Manager route is as follows:
spring:
cloud:
gateway:
server:
webflux:
routes:
- id: manager_route
uri: ${GATEWAY_ROUTE_MANAGER_URI:http://${CENTER_MANAGER_HOST:dc3-center-manager}:8400}
predicates:
- Path=/api/v3/manager/**
filters:
- StripPrefix=2
- AuthenticThere is no lb:// here, and no instance list is pulled from Nacos: dc3-center-manager is resolved by container DNS, while CENTER_MANAGER_HOST or GATEWAY_ROUTE_MANAGER_URI provides the environment overrides. If a registry or Kubernetes load balancing is adopted in the future, simply adjust the route discovery mode according to the deployment model.
Dividing Labor Between the Edge Gateway and the Cloud Gateway
The cloud API gateway handles authentication, northbound routing, rate limiting, and API version management; the edge gateway sits close to the devices and handles protocol conversion, data preprocessing, local caching, and store-and-forward across network outages. The two sets of responsibilities must not be conflated. Work such as Modbus RTU to MQTT conversion and field data filtering belongs at the edge; tenant authorization and platform API routing should stay in the cloud.
The engineering conclusion: use the name resolution and configuration injection capabilities the deployment platform already provides, and introduce an independent registry or configuration center only under real governance pressure. For the current IoT DC3, fixed service names, container DNS, environment variables, and Spring Cloud Gateway already form a complete — and simpler — service addressing scheme.
6.2.4 Containerization and Deployment of IoT Microservices
After services are split into microservices and the addressing and configuration scheme is settled, the next question to face is: how do a few dozen microservices get onto the servers? Every release means manually installing the JDK, setting environment variables, starting the JAR, and then watching the logs to confirm the process has not died. After repeating this a few times, one naturally starts looking for a more reliable way. Containerization is the engineering practice born precisely to solve this pain point. Service addressing can come from a Kubernetes Service, Compose DNS, or an independent registry — it cannot be presupposed that every project has already deployed a registry.
Containerization: making environment differences disappear
Docker packages an application together with its runtime environment into a single image. For IoT microservices, this means the JDK version used during development is fixed when the image is built; production no longer needs a JDK installed — pull the image and run it directly. The immutability of container images is the basic means of eliminating the "but it runs on my machine" problem, and the prerequisite for microservices to reach automated deployment. The following is a typical Dockerfile example (using the platform microservice dc3-gateway):
FROM eclipse-temurin:21-jre-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
ARG JAR_FILE=target/dc3-gateway.jar
COPY ${JAR_FILE} /home/appuser/app.jar
USER appuser
EXPOSE 9200
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget -qO- http://localhost:9200/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "/home/appuser/app.jar"]Several points in this Dockerfile map directly to IoT scenarios: the Alpine base image reduces size — edge environments with limited bandwidth are more sensitive to image dimensions; designating a non-root user lowers security risk; adding a health check lets container orchestration tools automatically judge whether the service is alive. Manually running docker run, however, is clearly unsustainable. Once the number of microservices passes a certain threshold, the way containers are managed needs to be upgraded to cluster orchestration.
Kubernetes: declarative deployment and self-healing
Kubernetes manages container clusters through a declarative API. You tell it "I want to run 2 dc3-gateway instances, each with 1 CPU core and 512 MB of memory," and K8s schedules the containers onto suitable nodes and continuously ensures the actual state matches the declared state.
apiVersion: apps/v1
kind: Deployment
metadata:
name: dc3-gateway
namespace: iot-platform
spec:
replicas: 2
selector:
matchLabels:
app: dc3-gateway
template:
metadata:
labels:
app: dc3-gateway
spec:
containers:
- name: gateway
image: registry.example.com/dc3-gateway:1.0.0
ports:
- containerPort: 9200
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 9200
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 9200
initialDelaySeconds: 15
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: dc3-gateway-svc
namespace: iot-platform
spec:
type: NodePort
selector:
app: dc3-gateway
ports:
- port: 80
targetPort: 9200
nodePort: 30080When deploying IoT microservices, the distinction between the liveness probe (livenessProbe) and the readiness probe (readinessProbe) deserves attention. The liveness probe decides whether to restart the container — when the service deadlocks, a restart recovers it; the readiness probe decides whether traffic is directed at the Pod — until protocol driver initialization completes, traffic should stay out. In IoT scenarios, a Modbus bus scan or an OPC UA session setup can take several seconds; if the readiness probe times out and fails prematurely, the Pod restarts over and over. A common practice is to expose the /actuator/health/readiness endpoint only after driver initialization completes.
Edge and cloud: deployment strategies for different tiers
Containerized deployment in IoT faces a particular reality: the gap between cloud and edge node hardware is wide. Cloud servers have many CPU cores, large memory, and stable networks; an edge gateway may have only a single-core ARM processor, 512 MB of memory, and a 4G/5G connection. In response to this gap, the industry has settled into two deployment strategies:
The device-edge-cloud layering below is a generic containerization reference, not IoT DC3's current Compose template. Only when the needs for node count, unified scheduling, and failure self-healing are large enough to cover the operations cost of a cluster is it necessary to evaluate Kubernetes or k3s.
- Deploy a Kubernetes cluster in the cloud: package the center services as containers with declarative orchestration, and deploy the monitoring and logging pipeline alongside.
- Deploy a lightweight container environment at the edge: evaluate k3s when resources are constrained and there is a genuine cluster scheduling need; a single node or a small number of Drivers can also use a simpler way of running containers.
Edge-Native and Offline Autonomy
Cloud Kubernetes is only half of AIoT deployment. The other half happens on gateways, edge servers, and field devices, and the core constraint of this tier is running safely even when the network is unstable.
- Lightweight runtimes: K3s, the edge-oriented trimmed-down Kubernetes, can run the control plane + data plane on a single node or a small number of nodes, with a toolchain consistent with cloud K8s — it suits medium-to-large campuses, factories, and workshops; ESP32, Raspberry Pi, or MCU-class devices are not suitable for running full K8s, and usually do fine with systemd, lightweight containers, or plain process management.
- Optional extension: Wasm/WASI: package untrusted or third-party logic (device rules, simple operators) as Wasm modules and constrain the capability surface with WASI interfaces — faster than restarting a container and smaller than a dynamic JVM/Python sandbox. It is a complementary option, not a default replacement for Docker; there is no need to push it when you do not need "hot-pluggable third-party rules."
- Offline autonomy: an edge node should be able to keep collecting, executing local rules, caching events, and maintaining device command receipts during a network outage, then synchronize by priority once the network recovers. The default policy should be "keep running when disconnected, and always refuse to execute actions without safety constraints," not "crash when disconnected."
- Status and heartbeats: every edge node must be able to report to the platform its firmware version, model version, driver list, heartbeat timeline, and latest error codes; the management plane runs change management on this data, without relying on operators to log in to the target node.
- Degradation paths: scenarios such as a gateway going offline, a cloud failure, or a missing model need explicit degradation modes, for example "keep only read-only queries" or "roll rules back to the last known safe version." Degradation is not an anomaly; it is one of the normal operating states.
For the current IoT DC3 deployment, edge-native options should be treated as a separate review item: when is it worth introducing K3s? When is it acceptable to go with the simpler "Compose + heartbeat + OTA" approach? The answers depend on the failure radius, release frequency, operations radius, and team size — having K8s does not automatically mean it takes priority.
Deployment Decision Checklist
When a project has just started, a single server running Docker Compose is usually enough. To judge whether an upgrade to K8s is warranted, check against these questions:
- Do multiple service instances need automatic load balancing?
- Can service updates tolerate the brief interruption caused by restarting everything at once?
- How many distinct runtime environments (development, test, staging, production) need managing?
- Does the team have the capacity to operate a Kubernetes cluster?
For the current IoT DC3, Compose, fixed service names, and environment variables already form a runnable baseline. Whether to upgrade to k3s, Kubernetes, or multi-cluster management should be decided jointly by node scale, release frequency, failure-recovery objectives, and the team's operations capability — not by treating a hybrid cluster as the default starting point.
Microservice containerization gives the IoT platform a flexible foundation. Once containerized deployment stabilizes, data pipelines and stream processing become the core problems the platform layer must solve — how data moves reliably from the edge into the cloud, and how initial analysis is completed in the stream (Chapter 5 already covered their general design); Section 6.3 of this chapter will show the engineering implementation using IoT DC3 as the example.