Skip to content

AxonOps — AI-Native Control Plane for Open Source Data Platforms

Build vs Buy - Kafka Integration

When integrating external systems with Kafka, organizations face a fundamental choice: use Kafka Connect with pre-built connectors, or write custom integration code using the Producer and Consumer APIs.


Every Kafka deployment requires moving data between Kafka and external systems—databases, cloud storage, APIs, legacy applications. This integration code must handle:

ConcernDescription
ConnectivityEstablishing and maintaining connections to external systems
SerializationConverting between external formats and Kafka records
Error handlingRetries, dead letter queues, failure recovery
Offset managementTracking position to enable resume after failures
ScalingParallelizing work across partitions and workers
MonitoringMetrics, logging, alerting on integration health
Exactly-once processingConnector-dependent duplicate avoidance

Building this infrastructure from scratch for each integration requires significant engineering effort. Kafka Connect exists to provide this infrastructure as a reusable framework.


Use the Kafka Connect framework with pre-built or custom connectors:

  • Connectors handle system-specific logic (database queries, API calls, file operations)
  • Framework handles common concerns (offset management, fault tolerance, scaling)
  • Configuration-driven—deploy integrations without writing code
  • Ecosystem of 200+ production-grade connectors

Write application code using the Producer/Consumer APIs directly:

  • Full control over behavior and performance characteristics
  • No framework overhead—direct API access
  • Application-specific logic integrated with business code
  • Responsibility for all infrastructure concerns

The build vs buy decision has long-term implications:

FactorImpact
Initial velocityConnect deploys in hours; custom takes weeks
Maintenance burdenConnect offloads to maintainers; custom requires ongoing ownership
FlexibilityCustom can do anything; Connect has framework constraints
Team expertiseConnect needs configuration skills; custom needs Kafka development expertise
Operational modelConnect standardizes operations; custom varies per integration

Neither approach is universally better—the right choice depends on specific requirements, team capabilities, and organizational context.


Decision flow for using an existing connector or building custom integration codeDecision flow for using an existing connector or building custom integration codeIntegration requirement identifiedConnector exists for target system?yesnoConnector has required features?yesnoAcceptable performance?yesnoUse existing connectorEvaluate custom orcontribute improvementEvaluate custom vsconnector modificationStandard integration pattern?yesnoConsider building connectorCustom code likely betterCustom implementation

BenefitDescription
Standardized deploymentSame process for all integrations
Built-in fault toleranceAutomatic task redistribution
Offset managementFramework handles position tracking
MonitoringStandard JMX metrics
ConfigurationREST API, no code deployment
ScalingAdd workers, increase tasks
BenefitDescription
Schema Registry integrationAutomatic serialization
Single Message TransformsLightweight transformations
Dead letter queuesStandardized error handling
Exactly-onceSupported for compatible connectors and configurations
ConvertersMultiple serialization formats
AspectConnectCustom
Initial developmentConfiguration onlyFull implementation
MaintenanceConnector updatesFull ownership
TestingConnector tested by communityFull test suite needed
DocumentationProvided by maintainerMust create

ScenarioRationale
Standard sinks (object storage, search, databases)Mature, tested connectors exist
CDC from databasesDebezium connectors are excellent
Cloud service integrationVendor-maintained connectors
Operational simplicity requiredSame management for all integrations
Team lacks Kafka expertiseConnect abstracts complexity

Using Connect (Recommended):

{
"name": "s3-sink",
"config": {
"connector.class": "io.confluent.connect.s3.S3SinkConnector",
"topics": "events",
"s3.bucket.name": "data-lake",
"format.class": "io.confluent.connect.s3.format.parquet.ParquetFormat",
"flush.size": "10000"
}
}

Custom Implementation Required:

  • S3 client setup
  • Batch management
  • File rotation logic
  • Offset tracking
  • Error handling
  • Retry logic
  • Exactly-once semantics
  • Parquet serialization
  • Partitioning logic
  • Monitoring metrics

The connector encapsulates thousands of lines of production-tested code.


ScenarioRationale
Complex business logicKafka Streams better suited
Sub-millisecond latencyDirect producer has less overhead
No suitable connectorNovel or proprietary system
Connector missing critical featureCustom may be faster than contribution
Deep system integrationApplication-specific behavior

Custom Code Better:

// Business logic interleaved with Kafka operations
streams.stream("orders")
.filter(order -> validateOrder(order))
.mapValues(order -> enrichWithInventory(order))
.filter(order -> checkFraudRules(order))
.mapValues(order -> calculatePricing(order))
.to("validated-orders");

This logic doesn’t fit the Connect model of simple record transformation.

Custom Code Required:

// No connector exists for legacy system
LegacyClient client = new LegacyClient(config);
Producer<String, byte[]> producer = new KafkaProducer<>(props);
while (true) {
LegacyMessage msg = client.receive();
ProducerRecord<String, byte[]> record =
new ProducerRecord<>("legacy-events", msg.toBytes());
producer.send(record);
}

Use Connect for I/O, Streams for processing.

Connect handling source and sink I/O with Kafka Streams performing processingConnect handling source and sink I/O with Kafka Streams performing processingSource SystemSource ConnectorRaw TopicKafka StreamsProcessed TopicSink ConnectorTarget SystemComplex business logicin Kafka StreamsStandard I/Ovia Connect

Write custom Single Message Transform for specialized logic.

public class CustomTransform implements Transformation<SinkRecord> {
@Override
public SinkRecord apply(SinkRecord record) {
// Custom transformation logic
return record.newRecord(
record.topic(),
record.kafkaPartition(),
record.keySchema(),
record.key(),
transformedSchema,
transformedValue,
record.timestamp()
);
}
}

Configuration:

{
"transforms": "custom",
"transforms.custom.type": "com.example.CustomTransform"
}

FactorConnectCustomHybrid
Simple source/sink✅❌⚠️
Complex processing❌✅✅
Sub-ms latency❌✅❌
Operational simplicity✅❌⚠️
Schema management✅⚠️✅
Exactly-once✅⚠️✅
Novel integration❌✅⚠️
Team expertise neededLowHighMedium

Legend: ✅ Recommended | ⚠️ Possible | ❌ Not Recommended


PhaseEffort
Initial setupHours to days
ConfigurationJSON/REST
TestingConnector validation
MaintenanceConnector upgrades
MonitoringStandard metrics
ScalingConfiguration change
PhaseEffort
Initial developmentWeeks to months
ImplementationFull code base
TestingUnit, integration, load
MaintenanceBug fixes, feature additions
MonitoringCustom instrumentation
ScalingCode changes possibly needed
Connect and custom connector effort over short, medium, and long time horizonsConnect and custom connector effort over short, medium, and long time horizonsShort-term (< 1 month)Medium-term (1-6 months)Long-term (> 6 months)Connect: 2-8 hours setupCustom: 40-200 hours developmentConnect: Minor config updatesCustom: Bug fixes, edge casesConnect: Occasional version upgradesCustom: Ongoing maintenance, feature requests

When evaluating whether a connector meets requirements:

CriteriaQuestions
FunctionalityDoes it support required features?
PerformanceCan it handle expected throughput?
ReliabilityWhat’s the production track record?
MaintainerIs it actively maintained?
LicenseCompatible with deployment model?
SupportWhat support options exist?
DocumentationIs configuration well-documented?
CommunityAre issues addressed promptly?

  1. Deploy connector alongside custom code
  2. Validate connector output matches custom
  3. Gradually shift traffic to connector
  4. Decommission custom code
  1. Implement custom code with same behavior
  2. Deploy alongside connector
  3. Validate output equivalence
  4. Switch over
  5. Remove connector