Make the right development approach decision. Covers use cases, limitations, governance, and hybrid patterns for Power Platform, OutSystems, and traditional development.
This reference describes when to choose low-code development platforms over traditional programming, or vice versa. It focuses on practical engineering decisions around tooling, data flow, configuration and deployment, without vendor-specific mechanics.
Low-code platforms excel when building applications that require basic data entry, listing, editing and deletion of records. The platform handles UI rendering, form validation and basic state management.
Example HTTP API endpoint for creating a record:
POST /api/v1/records
Content-Type: application/json
Authorization: Bearer <token>
{
"name": "John Doe",
"email": "[email protected]"
}
The platform typically generates endpoints like this:
GET /api/v1/records/{id}
PUT /api/v1/records/{id}
DELETE /api/v1/records/{id}
Platform configuration for form generation might include a JSON schema:
{
"fields": [
{
"name": "name",
"type": "string",
"required": true
},
{
"name": "email",
"type": "email",
"required": true
}
]
}
Low-code tools provide ready-made integrations with common services like email, calendars, databases and cloud storage. These are typically exposed as connection objects in the platform's configuration.
Example configuration for a database connection:
connections:
- name: postgres_db
type: postgres
host: db.example.com
port: 5432
database: app_data
username: app_user
The integration layer usually exposes standard SQL-like operations through its API:
GET /api/v1/connections/postgres_db/query
Content-Type: application/json
Authorization: Bearer <token>
{
"sql": "SELECT * FROM users WHERE active = true"
}
Low-code platforms often include visual workflow editors that translate drag-and-drop actions into executable processes. These may be represented as state machines or event-driven flows.
Example workflow definition:
{
"start": "user_submitted_form",
"transitions": [
{
"from": "user_submitted_form",
"condition": "form.is_valid",
"to": "send_notification"
},
{
"from": "send_notification",
"to": "complete_workflow"
}
]
}
Pro-code is necessary when the application requires domain-specific logic that cannot be expressed through low-code visual tools. This includes complex mathematical operations, machine learning inference, or proprietary algorithms.
Example SQL for a custom business calculation:
UPDATE inventory SET
available_quantity = current_stock - reserved_orders,
last_updated = NOW()
WHERE item_id IN (
SELECT DISTINCT item_id FROM orders
WHERE status = 'pending'
);
The code might be implemented in Python:
def calculate_inventory_adjustment(items):
for item in items:
item['available'] = item['stock'] - item['reserved']
if item['available'] < 0:
raise ValueError("Negative inventory not allowed")
return items
Pro-code is required when you need to implement non-standard HTTP interfaces, custom headers, or complex authentication flows.
Example API endpoint with custom logic:
POST /api/v1/custom/process
Content-Type: application/json
X-Custom-Auth: Bearer <token>
Authorization: Basic <credentials>
{
"data": "some_input",
"timestamp": "2023-01-01T00:00:00Z"
}
Response might include custom headers:
HTTP/1.1 200 OK
X-Processed-Timestamp: 2023-01-01T00:01:00Z
Content-Type: application/json
{
"status": "processed",
"result": {
"id": "abc123",
"timestamp": "2023-01-01T00:01:00Z"
}
}
When your application requires complex data modeling that goes beyond what low-code platforms can represent, pro-code becomes necessary.
Example database schema with complex relationships:
CREATE TABLE user_profiles (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
preferences JSONB,
metadata JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE audit_log (
id SERIAL PRIMARY KEY,
table_name VARCHAR(100),
record_id INTEGER,
action VARCHAR(20),
old_values JSONB,
new_values JSONB,
timestamp TIMESTAMP DEFAULT NOW()
);
Low-code platforms often assume simple CRUD operations. When you need custom queries, bulk operations or real-time streaming, pro-code is required.
Example of non-standard query that low-code tools might not support:
WITH RECURSIVE hierarchy AS (
SELECT id, parent_id, name, 0 as level
FROM categories
WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.parent_id, c.name, h.level + 1
FROM categories c
JOIN hierarchy h ON c.parent_id = h.id
)
SELECT * FROM hierarchy ORDER BY level, name;
Low-code platforms may not provide the control needed for high-throughput applications or memory-constrained environments.
Example of a performance-critical loop in Python:
def batch_process_records(records):
# Process in batches to avoid memory issues
batch_size = 1000
results = []
for i in range(0, len(records), batch_size):
batch = records[i:i + batch_size]
# Custom processing logic
processed_batch = [process_record(r) for r in batch]
results.extend(processed_batch)
return results
When connecting to internal systems that don't expose standard APIs, or when you need to work with binary protocols, pro-code is required.
Example of a custom integration with an internal service:
import socket
def connect_to_internal_service(host, port):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))
# Send custom protocol message
message = b'\x01\x02\x03\x04' + b'custom_data'
sock.send(message)
response = sock.recv(1024)
return response
The biggest silent failure in low-code systems is that they can't easily handle exceptions in custom logic. When a workflow step fails, the error handling may not be visible to the user or developer.
Example error message when workflow fails:
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{
"error": "Workflow execution failed",
"details": "Could not process record in step 'send_notification'"
}
Pro-code systems often lack built-in UI generation, requiring developers to build and maintain user interfaces separately. This can lead to inconsistencies between business logic and presentation.
When integrating with low-code platforms, the pro-code service might silently fail to handle errors from the low-code system:
POST /api/v1/integration/trigger
Content-Type: application/json
{
"workflow_id": "abc123",
"data": {
"user_id": 456,
"action": "submit_form"
}
}
If the low-code system returns an unexpected format, the pro-code service may silently drop invalid fields or fail to process them correctly.
Low-code platforms often batch operations in ways that can cause performance issues when data volume grows. The platform may not expose configuration options for tuning these behaviors.
Example of a poorly performing low-code operation:
GET /api/v1/records?limit=1000&offset=0
As data grows, the platform might not support efficient pagination or indexing, leading to slow responses.
Pro-code applications often require complex CI/CD pipelines that must be maintained separately from low-code platforms. The integration between them may have subtle timing issues or error handling problems.
Example deployment configuration for a mixed system:
deployments:
- name: api-service
type: docker
image: mycompany/api:latest
env:
DATABASE_URL: postgres://db.example.com:5432/app
LOWCODE_API_URL: https://lowcode.example.com/api
- name: lowcode-app
type: platform
configuration:
database_connection: postgres_db
ui_template: standard_form
When a low-code workflow calls into pro-code, error propagation can be incomplete. The low-code system may log errors but not expose them to the calling application.
Example of an integration failure:
POST /api/v1/workflow/execute
Content-Type: application/json
{
"workflow": "user_registration",
"input": {
"name": "Jane Doe",
"email": "[email protected]"
}
}
If the pro-code service fails during processing, the low-code system might return a generic success response even though the operation partially failed.
Low-code platforms often have limited transaction support. When you need to ensure data consistency across multiple operations, you must implement this in pro-code.
Example of a two-phase commit pattern:
def process_user_registration(user_data):
# Phase 1: Create user record
user = create_user(user_data)
# Phase 2: Send notification (if this fails, we want rollback capability)
try:
send_welcome_email(user.email)
# If we get here, commit the transaction
return {"status": "success", "user_id": user.id}
except EmailServiceError:
# Rollback user creation
delete_user(user.id)
raise
This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy