The programming languages and frameworks that you adopt to build your app play a pivotal role in your app’s successful delivery and maintenance over time. Language and framework choices have long-term consequences for how you scale your business, operate your app, and deliver high-quality features to your customers. Changes to either language or framework are typically expensive. Supporting parallel ecosystems of multiple languages and frameworks adds complexity and reduces agility.
The choice of language and framework has an impact on a range of factors, including delivery speed, stability and strength of the existing ecosystem, operational readiness, and production performance. Use low-code platforms where possible so that you can focus on solving your business problems instead of confronting the complications of traditional development. If your app’s requirements are more complex, choose mature languages and lightweight frameworks.
Low-code platforms enable you to build, test, and deploy enterprise apps faster than traditional manual coding. These platforms are well suited for building opportunistic apps in collaboration with business stakeholders as well as data reporting and analysis apps. Low-code platforms also enable you to extend SaaS apps and modernize legacy apps. This approach helps you avoid complications when you want to add new capabilities, such as data visualization, data collection, data analytics, security, accessibility, performance, and globalization. A low-code platform significantly alleviates these complications and dramatically reduces the amount of code you maintain.
However, if your app has more sophisticated requirements, choose a mature programming language coupled with a lightweight framework. When choosing a programming language, select one that provides you with key benefits, such as:
Newer languages tend to have a higher rate of change in their language design and corresponding ecosystem and libraries. A higher rate of change means that it is more difficult to evaluate risk and more expensive to make subsequent changes.
When choosing a framework, select one that is open source. Open source frameworks are under constant peer review, which means that the features are close to what most developers want because they contribute to the frameworks’ creation and maintenance. Bugs are found and fixed quickly. You also want to select a lightweight framework that consumes few resources, such as CPU, memory, network bandwidth, or file handles.
Use an app framework that balances improving task focus (business logic over boilerplate and scaffolding) with flexibility (allowing you to support current and future feature needs). Adopt a framework that provides easy-to-consume, sensible, and uncontroversial defaults for common features, such as logging, telemetry, security, configuration, as well as for common patterns, such as building REST APIs.
Oracle APEX is a low-code platform that provides you with high-level components, such as forms, charts, and UI widgets. APEX also provides common design patterns through an intuitive graphical development environment. Apps developed using APEX can access local data via SQL and integrate with external services using REST APIs. Additionally, you can publish the functionality you develop in APEX as REST APIs for external consumption.
If a low-code platform is inappropriate for your app, adopt Java as your programming language. Java provides a stable and broad set of capabilities for most common app use cases and has a healthy ecosystem of trusted and stable libraries and frameworks for developing modern apps. Java's focus on simplicity and readability coupled with its excellent support for developer tooling—including static analysis tools and testing frameworks—reduce software maintenance costs and the risk of bugs in production apps.
Use GraalVM to develop and run your app. GraalVM is a JDK distribution that couples the stability of Java with best-in-class performance through dynamic runtime optimization, frequent and proactive patching of security vulnerabilities, and low-cost performance analysis and diagnostic tooling, such as Java Flight Recorder.
Build your app with an API-first approach using either Graal Cloud Native or the Helidon framework. Both approaches provide scaffolding that significantly reduces the time to deliver your app and easy-to-use patterns for common use cases, such as REST APIs, based on a set of simple, uncontroversial framework choices for common activities, such as logging, telemetry, and storage. Additionally, both approaches support high-performance services, through support for nonblocking I/O with idiomatic reactive APIs, and low latency, through support for high-performance network libraries.
Both Helidon and GCN have built-in support for GraalVM native image, which enables you to build memory-efficient and compact apps.
Split your app’s features or tasks into independent, loosely coupled services that work in conjunction with each other. Design each service with a limited functional scope that focuses on one feature or capability. In comparison to a traditional monolithic architecture, this approach improves app maintenance, feature development, testing, deployment, and scalability.
Take a contract-first REST API design approach to provide clear and understandable interfaces for communicating with and between services. An API contract provides the necessary mechanism for teams to collaborate and consume functionality without depending on the internal details of a service’s implementation. For example, a service can be wholly owned by a development team that can freely improve on its implementation without having to coordinate code dependencies with other development teams.
Begin with a contract-first approach by specifying the REST API of a service. Then prototype an implementation of the API to let your stakeholders, such as the teams who will use it, try it out. When everyone agrees on the details of the API, independent teams can work in parallel to implement the service and other services that will consume it.
Define your policy enforcements for security and service level agreements early in your product lifecycle to ensure that everyone is clear on all aspects of the service contract.
Treat your API specification as code and manage it in a version control system along with your source code and policy configurations.
Specify your API using the implementation-agnostic format OpenAPI and store it in a repository provided by Oracle Cloud Infrastructure (OCI) DevOps.
Implement your services using a lightweight open source approach, such as Graal Cloud Native or Helidon.
Deploy your services on serverless platforms, such as Oracle Container Engine for Kubernetes or Oracle Functions, for ease of deployment, scalability, and cost efficiency.
Use Oracle Cloud Infrastructure API Gateway to create protected and governed private or public endpoints from the API specification.
Use Oracle Cloud Infrastructure Service Mesh to simplify and secure communication between services hosted in your Oracle Container Engine for Kubernetes cluster. OCI Service Mesh also allows you to observe all network traffic between your services via the metrics and logs emitted by its proxy component that runs as a sidecar on application pods.
A container packages code and its dependencies as one unit, so an app runs quickly and reliably across multiple computing environments. A container image is a file that, when executed, will create and start a container on a computing environment.
Compared to traditional virtual machines, containers are smaller, require fewer resources, and have faster start times. They are also platform independent and can run apps anywhere. To take advantage of these benefits, decompose your app into services that each perform a discrete business function and package each service as a container. For legacy apps, gradually replace each existing function in your app with a containerized service until the whole app is refactored.
Packaging application code and dependencies into a single executable unit (a container image) means that a container is extremely portable. By combining this portability with infrastructure abstraction, containers bring operational consistency to your app. Whether your app is running on-premises on a physical server or in the cloud on a virtual machine, it produces the same results every time.
Through this consistent reproducibility and predictability, containers simplify DevOps processes and enable your development teams to deploy your apps faster. By providing process-level isolation, and because they’re frequently replaced, containers simplify and expedite the processes associated with remediating software vulnerabilities. Breaking down apps into modular containerized services also makes them very robust. An error or failure in an individual service doesn’t bring down your entire app, and you can update or patch each service independently from the rest of the app.
Unlike a virtual machine, a container doesn’t come with an operating system of its own; instead, it shares the operating system of its host. As a result, containers are smaller in size and faster to start than virtual machines. Most container images are tens of megabytes in size compared to virtual machines that can be several gigabytes, and their start times are in seconds instead of the minutes it takes to start virtual machines.
The best way to run and scale your app with modularized services in containers is to deploy one service per container. This approach isolates services from each other, which eliminates downtime and enables independent scaling for each service.
Although you can deploy containers manually, it’s better to use container management software that integrates with your continuous integration and continuous deployment tools.
Use Oracle Cloud Infrastructure Registry (Container Registry) to store your container images and Oracle Cloud Infrastructure Container Engine for Kubernetes (OKE) to run and manage your containers. These fully managed services are tightly integrated with OCI platform capabilities, are available in all Oracle Cloud regions, and meet regulatory standards, such as PCI, ISO, SOC, HIPAA, and FedRAMP.
In addition to storing container images in Container Registry, you can store manifest lists (sometimes called multi-architecture images) to support multiple architectures, such as ARM and AMD64. To identify and mitigate potential security vulnerabilities, enable image scanning on all images uploaded to Container Registry. You should also sign your container images to ensure that only authorized and trusted images are deployed to OKE.
Continuous integration (CI) and continuous deployment (CD) are a set of tools and procedures that development teams use to deliver code changes frequently and reliably. CI/CD best practices include code reviews, guidance for unit testing, integration testing, code check-ins, filing tickets, and deploying applications to development and test environments.
Continuous integration describes a practice in which developers frequently integrate their code changes into the main branch of a shared repository. The developers’ changes are validated by creating a build and then running automated tests against the build. The tests ensure that your app is not broken whenever new changes are integrated into the main branch.
The advantages of continuous integration include:
Continuous delivery is a step beyond continuous integration; after passing the appropriate tests, a build is automatically delivered to a testing and/or production environment. The goal of continuous delivery is to always have a codebase ready for deployment to a customer production environment.
Additional advantages of continuous delivery include:
Continuous deployment goes one step further than continuous delivery; every change that passes all the tests is automatically deployed to your customer production environment. There is no human intervention—only a failed test can prevent a new change to be deployed to production. With no human intervention, continuous deployment relies heavily on well-designed test automation.
Additional advantages of continuous deployment include:
CI/CD provides best practices for storing, integrating, deploying, and maintaining code to automate how you build your apps. These best practices include the following:
Use the DevOps service to automate deployment of your cloud native apps. First, store the code in a DevOps code repository and create a release branch. Work in small increments to make changes to the release branch and reconcile issues in the branch daily to ensure its stability. Then use the triggers functionality in the code repository to automatically start a DevOps build pipeline.
Use a single DevOps build pipeline to build all artifacts associated with the code repository. If the build fails, configure the build pipeline to wait for approval before it completes. The approval request should go to the committer who triggered the build, and they should resolve the issue with a new code commit and then approve the completion of the build. For successful builds, configure the build pipeline to automatically deliver the artifacts to the Oracle Cloud Infrastructure Artifacts Registry service and to automatically trigger a DevOps deployment pipeline.
Add Application Dependency Management to detect security vulnerabilities (CVEs) in application dependencies during a build stage in an OCI DevOps build pipeline. This way, you will be able to detect and remediate potential CVEs as soon as they become known.
Use Resource Manager to create all your infrastructure environments in a maximum-security zone to automatically benefit from deployment security. By using Resource Manager, you can use infrastructure as code to automate infrastructure creation across all your regions in a consistent manner. Then you can configure the DevOps deployment pipeline to always deploy to the resources that you have created in the security zone.
Create a single DevOps deployment pipeline that deploys all the artifacts created from a single build pipeline. Organize deployment environments, such as OCI region, availability domain, and fault domain. Configure the pipeline with a canary, rolling, or blue-green deployment strategy. Also configure it to trigger tests automatically. Enable OCI Monitoring and Application Performance Monitoring on your application and infrastructure to detect issues.
If no issues are detected and tests complete successfully, configure the deployment pipeline to automatically deploy the artifacts to the next environment in the deployment strategy until the artifacts are fully deployed in all production environments. When deploying to the production environment, configure the pipeline to deploy to all fault domains in an availability domain one at a time. Deploy to each availability domain in a region one at a time, and then, to each region one at a time. Continue to use OCI Monitoring and Application Performance Monitoring on the application and infrastructure to quickly detect issues. Set up alerts, and if any key metrics suddenly drop during deployment, automatically fail the deployment and trigger a rollback to the previous version.
A managed service provides specific functionality without requiring you to perform maintenance tasks related to optimizing performance, availability, scaling, security, or upgrading. With a managed service, you can focus on delivering features for your customers instead of worrying about the complexity of operations.
A managed OCI service provides a scalable and secure component for cloud native development. Use managed services to develop and run your app and store its data; you get best-in-class solutions without needing expertise in each domain to build and operate your app.
Managed services enable you to create highly available, scalable, agile, and performant applications with security, compliance, and resiliency.
Managed services abstract the complexity of your app’s underlying components, making it easy to store and retrieve data or create and run your app. The services integrate with toolsets that provide automated building, testing, and deployment of your app. Managed services improve productivity and reduce time to market.
Managed services centralize and automate various infrastructure management tasks, removing human error and the need for specialized skills. The underlying infrastructure is kept up to date and secure, and the services enable you to monitor and track modifications or access, which ensures the confidentiality and integrity of your app and data.
Managed services are highly available and scalable, meeting the needs of your app, and you pay only for what you use. You can start small and scale without experiencing any degradation in performance or reliability.
We recommend the following cloud services:
These services are highly available, high performing, and elastic. Their underlying infrastructure is managed and patched to ensure that your app remains secure.
An app’s state can consist of many elements, including data caches, a user’s preferences, personalization, messages exchanged between services, the position in a multiple-step workflow, app deployment, runtime configuration, and a user’s session (for example, the page a user last visited or the size and items in a user’s shopping cart). If the state of your app is lost, it can result in loss of data, a malfunction in your app, a suboptimal user experience—and sometimes, a complete app failure.
If you store your app’s state on local file systems or in the memory of a single host, then it might be lost if your app experiences outages, such as restarts or localized disk failures. Instead, save the state in external persistence stores. Use as few persistence stores as possible; ideally, just one to provide data consistency.
The elements of an app’s state are traditionally stored as multiple artifacts in various formats, such as serialized objects, JSON or XML documents, or text files. If these elements are stored across multiple persistence stores, such as external file systems, message stores, object stores, multiple databases, or elastic block storage, then it’s possible for the different data stores to get out of sync and result in state inconsistencies. An app must also implement transactions, joins, and idempotency to ensure data consistency when the state needs to be updated as a unit.
By scattering elements of an app’s state across multiple stores, the opportunities increase for security vulnerabilities. Lifecycle operations—such as adding and removing nodes, patching, backup and recovery, and replication for disaster and recovery—become extremely complex and require special consideration to keep state consistency across different stores.
As a result, a better approach is to store all the app’s state and its data in a single database, if possible. Data stays consistent in a single store and is easier to manage. With this approach, app instances can be replaced. This especially helps with modern app architectures, such as elastic microservices or ephemeral instances where an instance exists only for serving a single or a few requests. Adding a node is simplified because a new node can get the latest copy of the state—and removing a node doesn’t result in losing the state entirely. Patches can be applied in a rolling fashion just by replacing the executable(s). A node can be restored from backups and acquire the state from the database. State can be consistently replicated as a unit to different regions for disaster recovery. Having a consistent state in different regions can ensure there won’t be any functional problems in your app after a failover or switchover.
If your app uses a database, use the same database to store its state. A database provides better availability, integrity, and security than alternatives, such as files or in-memory representations. Ideally, use a multi-model database (which can store different formats) to store all the elements of your app’s state. Using a multi-model database instead of multiple single-purpose data stores also lets you easily achieve and maintain consistency across all the elements of your app state. (Note: Although it’s permissible to store cached state in the app, the app must be designed to use the database as the source of truth and be able to recreate its state from the database.) Oracle Database is an ideal fit for this purpose. It stores different formats and provides predictable performance, so storing your app’s state in it doesn’t degrade the performance of your app.
If your app doesn’t use a database, use other durable persistence stores, such as Oracle Cloud Infrastructure Object Storage, to store the state. If the app state can’t be kept in a single data store, design your app to store the state in multiple data stores that can be kept in sync and recovered as a consistent unit after failure.
Here are some recommendations for storing state in an Oracle Database.
Your app might use data in a variety of formats, such as tabular (relational), unstructured, XML, JSON, spatial, or graph. Traditionally, this variety required a different kind of database for each data format. For example: a relational database for relational data, a document store for unstructured data, or a graph database for hierarchical linked data. However, the use of multiple databases often leads to additional operational complexity and data inconsistency. Instead, use a single, multi-model database to store, index, and search multiple types and formats of data.
Take advantage of database functionality to simplify your app logic. For example, use SQL for queries, joins, and analysis; use transactions to guarantee consistency and isolation; and use built-in machine learning algorithms and analytics capabilities to avoid unnecessary data transfers. To protect sensitive data, use the database’s security features and access control, and use replication to improve the availability, scalability, and resiliency of your apps.
Use a multi-model database to store different types of data, such as JSON documents, property graphs, and relational data. Advanced multi-model databases offer full-featured support for any type of data stored in the database. You can store a new JSON document, insert relational rows, and update a property graph all within the same ACID transaction. You can use SQL statements to join, filter, and aggregate across these different types of data—this provides the strong consistency and concurrency guarantees that you’re accustomed to from relational databases. Along with offering this rich set of features, a multi-model database can also be used as a single-purpose data store—accessed using APIs other than SQL, such as REST APIs, document store APIs, and graph APIs.
A key advantage of using a multi-model database is its reusability. Although data might be of different types and shapes, the underlying technology to manage that data doesn’t change. This means that you don't have to learn multiple database technologies and understand how to use and tune each one for each type of data. And because the technology remains constant, you don’t have to rewrite your app code. Also, a multi-model database improves the resiliency of your app by reducing data fragmentation, thus making backup and recovery easier.
Use Oracle Autonomous Database, a multi-model converged database, to store, manage, and analyze all your data. Ease the maintenance of your app by using views to expose the data in tables so that the underlying schema can be changed without affecting your existing apps. Use edition-based redefinition so that you can upgrade your app with no downtime. Use Oracle Data Safe to implement and evaluate security controls, mask sensitive data, and audit data accesses. Use Oracle Data Guard as a highly scalable read cache for your data and to maintain a consistent backup for disaster recovery.
Oracle Autonomous Database performs operational tasks with no impact or interruption to its workload. This means that you don't need to add complex compensational logic to the app to handle scaling or failover scenarios. The database manages resources, such as CPU and storage, independently and provides elastic bidirectional scalability.
A single user request can follow a complex path across the multiple services or microservices that make up a modern app. End-to-end tracing follows the journey of each request from its source into the depths of your infrastructure and helps you to debug the root cause of a problem. Monitoring is generally used as a diagnostic tool, alerting your developers when your app isn’t working as expected.
Developers, administrators, and security officers must maintain an authoritative and timely understanding of your app’s health, performance, operational state, and possible security incidents. This understanding ensures that your app’s function and performance meet expectations during its lifecycle; it can also streamline incident diagnosis and application recovery. Comprehensive, end-to-end monitoring and tracing should be straightforward to implement and manage without adding complexity to your app.
Your app can fail to behave as expected in numerous ways. For example, it can perform poorly or just fail outright. Unlike a traditional monolithic app, an app built from microservices presents additional diagnostic challenges because of multiple interactions between its components.
Tracing is the best way to quickly understand what happens to a user request as it travels through the microservices and other components, such as infrastructure, that make up your app. Use end-to-end tracing to collect data on each user request and then review the data to see where your app may be experiencing bottlenecks and latencies. For example, a request may pass back and forth through multiple microservices before being fulfilled. Without a way of tracing the entire journey of a request, there is no way to determine the root cause of its failure.
Monitoring is generally much more directed, improving your understanding of how your app behaves by instrumenting your app and then collecting, aggregating, and analyzing metrics. End-to-end monitoring also allows intelligent and automated integration with tools that dynamically adjust resource capacity and coordinate responses to unexpected events.
Having a clear, accurate, and timely understanding of an app’s operational state and history is not just about measuring an end user's experience. You may also need to maintain compliance with regional or national jurisdictions which might require the ability to generate on-demand, detailed activity reports or attestations about the handling of specific, sensitive data elements.
In general, monitoring solutions should be compatible with third-party tools and align with your environment’s administrative tools in particular. It’s important to maintain design flexibility and avoid vendor lock-in.
Build comprehensive monitoring and tracing capabilities into your app from the outset and keep them consistent throughout its lifecycle. The capabilities should not add any complexity to development, testing, and deployment and be easy to implement and manage. Where possible, adopt solutions that extend to accommodate the diversity of platforms that you currently use and might deploy in the future.
OCI services, such as Monitoring, are designed to provide out-of-the-box support for monitoring. Also, you can extend many OCI services to your app components by using a consistent deployment and management experience through supported APIs and SDKs. For example, you can add automated monitoring metric collection or log capture with centralized storage for all your virtual machines and apps.
During development and testing, you can configure services to only collect basic debugging or performance testing information. As your app approaches production deployment, increase the scope, frequency, and traceability of information collected by making simple updates to existing configuration parameters.
Oracle Cloud Infrastructure Service Mesh automatically captures a variety of communication metrics and logs for services running in Oracle Container Engine for Kubernetes. You can use this data to track the health of your services in the mesh and improve application performance.
Use robust, centralized data collection for your entire cloud tenancy environment to provide a single location for analysis, coordinated investigation, and alert generation. Service Connector Hub enables flexible, consistent, and customizable responses to events. OCI Logging Analytics enables efficient analysis and investigation of all your cloud (and external) event logging systems. You can also use OCI Service Connector Hub, Functions, and Notifications to transform ingested metrics and logs into actionable alerts. And you can take advantage of our integrations with third-party products and services, such as Splunk and Grafana.
The following OCI services help you consolidate your logging, monitoring, and tracing across the environments that host your app: Logging, Monitoring, Logging Analytics, Application Performance Monitoring, OS Management, Database Management, and Java Management Service. These fully managed services are integrated with common OCI infrastructure resources and provide supported mechanisms to integrate your custom app resources.
A single point of failure is one component of an app which, upon failure, renders your entire app unavailable or unreliable. When you develop an app to be highly available and reliable, use automated data replication to ensure that the failure of a single component does not lead to data loss.
Data replication across machines and the use of redundant networking protects you against routine machine and network failures. Replicating your data in data centers (or “availability domains”) across multiple geographical regions protects you against localized disasters, such as fires, earthquakes, floods, or hurricanes.
For your app to achieve high availability, you must ensure that the data on which your app depends remains available when failures occur. The key to the high availability of data is redundancy via automated data replication.
Replication is the process of copying and maintaining database objects, such as tables, in multiple databases that make up a distributed database system. Changes applied at one site are captured and stored locally before being forwarded and applied at each of the replicas located at remote locations.
Replicated databases can function in two different modes: active-passive and active-active. In active-passive mode, there is a single primary replica and one or more secondary replicas; only the primary replica participates in app data processing. In active-active mode, all replicas participate in data processing. Active-active mode provides better resource use and higher availability because no primary-secondary failover is needed.
Redundancy ensures that data replicas fail independently. Machine or disk failures are typically independent, but a network or power failure might cause a group of machines to fail simultaneously. To protect against such incidents, network and power infrastructure should also be redundant, and data replicas must be carefully placed across different machine and locations that can’t fail together.
OCI data centers are carefully engineered to eliminate single points of catastrophic failure. A typical data center or availability domain contains multiple independent failure units known as fault domains. Two independent fault domains can’t fail together. Likewise, a single region might have multiple availability domains which are geographically separated to ensure that two of them can’t fail at the same time.
Use OCI storage services, such as Block Volumes, Object Storage, and File Storage, to replicate data across fault and availability domains so that no single point of failure can impact the availability of your app’s data. Take advantage of the resilient fault isolation built into OCI by using Container Engine for Kubernetes to deploy your app across multiple fault and availability domains. Oracle Autonomous Database, Data Guard, and GoldenGate provide active-active hardware and software replication for high availability as well as zero-downtime patching and upgrades. Use these managed services for highly available data without the need to build and maintain your own storage infrastructure.
Defense in depth is an approach where multiple, independent, redundant security controls act as layers of defense for an app. The layers are designed to provide security even if one of them fails, for example, a firewall combined with intrusion detection.
However, the manual management and configuration of security controls can be complex, opaque, and error prone—individually and collectively. Instead, secure your app and its data by using automated security controls.
Defense in depth manages risk by using diverse controls that cover the physical, technical, administrative, operational, personnel, and procedural elements of security. Their independence means that they provide a deep defense that handles failures, exploits, or other security vulnerabilities. The controls are designed to approach risks in different ways and provide logging, auditing, and other features to ensure that attempted security violations are detected and reported to appropriate stakeholders.
Rather than having to manually configure a complex set of security controls, use simple and prescriptive automated controls to secure your app. Automated security controls eliminate human error (a root cause of many security incidents) and help you secure your app and its data without requiring you to become a security expert.
Implement easy-to-use automated security controls in all stages of your app lifecycle, including development, build, test, deployment, and runtime. Verify users, permissions, and access policies at each step in the lifecycle and ensure that access is granted only when appropriate. Detect security issues that are introduced early in the software development lifecycle. Early detection ensures that your app is deployed in production with security architecture best practices and that operational security issues caused by misconfigurations or disclosed vulnerabilities are detected and mitigated.
OCI offers multiple automated security services to secure your app and its data. Here are some recommendations for using OCI services: