Skip to content
CMO & CTO
CMO & CTO

Closing the Bridge Between Marketing and Technology, By Luis Fernandez

  • Digital Experience
    • Experience Strategy
    • Experience-Driven Commerce
    • Multi-Channel Experience
    • Personalization & Targeting
    • SEO & Performance
    • User Journey & Behavior
  • Marketing Technologies
    • Analytics & Measurement
    • Content Management Systems
    • Customer Data Platforms
    • Digital Asset Management
    • Marketing Automation
    • MarTech Stack & Strategy
    • Technology Buying & ROI
  • Software Engineering
    • Software Engineering
    • Software Architecture
    • General Software
    • Development Practices
    • Productivity & Workflow
    • Code
    • Engineering Management
    • Business of Software
    • Code
    • Digital Transformation
    • Systems Thinking
    • Technical Implementation
  • About
CMO & CTO

Closing the Bridge Between Marketing and Technology, By Luis Fernandez

Versioning and Dependencies in OSGi

Posted on January 2, 2011 By Luis Fernandez

New year. Quiet office. The build light was green for days and I thought we were ready to ship our OSGi based app on Monday. I pulled a last minute update for a library that fixed one tiny bug in our billing module. Dropped the new bundle in the Equinox dropins folder, hit restart, and boom. ClassNotFound in a place that worked all week. The log pointed at a package that clearly existed. Same vendor. Same bundle name. A simple micro bump. My coffee went cold while I tried to wire the puzzle in my head.

That night taught me more about OSGi versioning and dependencies than months of reading specs. It was not a platform problem. It was mine. I had treated versions like decorative stickers. OSGi treats them as contracts. Keep the contract and life is good. Break it and the resolver will punish you at the exact moment you least expect it.

What versions mean in OSGi

In OSGi we deal with two kinds of versions. The one on the bundle and the one on the packages that the bundle exports. The second one is the important one for wiring. Package versions are what your consumers import. If you only bump the bundle version and never version the packages, you are flying blind.

OSGi uses four numbers: major dot minor dot micro plus a qualifier. The unwritten rule many of us follow is simple. Break binary compatibility, bump major. Add compatible API, bump minor. Bug fix that does not touch signatures, bump micro. Qualifier is for build tags. A timestamp is common in CI.

Export packages like you mean it

If your bundle exports a package, put a version on that export. Do not hide public classes in private packages. Version the API you expect others to use. Tools like bnd or the Maven Bundle Plugin make this painless.

# MANIFEST.MF
Manifest-Version: 1.0
Bundle-SymbolicName: com.example.billing
Bundle-Version: 1.3.2
Export-Package: \
 com.example.billing.api;version="1.3.0", \
 com.example.billing.spi;version="1.0.0";uses:="com.example.billing.api"
Private-Package: com.example.billing.internal.*

Notice the uses directive. The resolver uses it to keep the graph consistent. If your API returns types from another package, uses tells OSGi to align wiring so that everyone agrees on the same provider of those types.

Import packages with ranges

Consumers should import packages with a version range. A common pattern is from the minor you coded against up to the next major, not including it. That keeps you safe from breaking changes in the future and allows compatible updates.

# Consumer MANIFEST.MF
Import-Package: \
 com.example.billing.api;version="[1.3,2)", \
 com.example.billing.spi;version="[1.0,2)"

Square bracket means inclusive. Closing parenthesis means exclusive. Think of it like math class. You trust any 1.x where x can move. You do not trust 2.0 without a review.

Do not be lazy with wildcards. Importing everything without ranges turns your runtime into a slot machine. It may spin fine on your laptop and refuse to resolve in production when an extra bundle shows up.

Import Package beats Require Bundle

Import Package is about API contracts. Require Bundle is about a particular provider. The first lets the resolver pick any bundle that exports the right package with the right version. The second hard pins you to a vendor. The moment that vendor splits packages or shuffles exports, your wiring gets fragile.

Stick to package imports unless you are delivering a product where you control both sides and accept the tighter coupling. Even there, think twice.

Split packages and how to avoid them

A split package is when two bundles export the same package. It looks harmless until class space gets confused. Avoid it. Create clear API packages and keep internals private. If you need to extend an API, add a new subpackage. Keep one exporter per package.

Optional imports and dynamic imports

Optional imports are handy for extras like logging bridges. Use them with care. If your code truly works without the package present, mark it optional. If not, do not trick the resolver.

Import-Package: \
 org.slf4j;version="[1.5,2)";resolution:=optional, \
 *

Dynamic import is a last resort for situations like plugin class loading where you cannot predict names. For normal app code, it hides wiring problems that will surface later. Prefer explicit ranges.

Tooling that keeps you honest

If you build with Maven, the Maven Bundle Plugin from Apache Felix wraps bnd and generates a proper manifest. You can put your exports, privates, and import policies right in the POM. Eclipse PDE and Equinox handle this too inside RCP builds. On the server side, Apache Karaf and ServiceMix make it easy to drop bundles and watch the resolver at work.

<plugin>
  <groupId>org.apache.felix</groupId>
  <artifactId>maven-bundle-plugin</artifactId>
  <version>2.3.7</version>
  <extensions>true</extensions>
  <configuration>
    <instructions>
      <Export-Package>
        com.example.billing.api;version=${project.version}
      </Export-Package>
      <Private-Package>com.example.billing.internal.*</Private-Package>
      <Import-Package>
        com.example.billing.api;version="[${project.version;==;${@}} , 2)", \
        *
      </Import-Package>
    </instructions>
  </configuration>
</plugin>

bnd has a neat feature called baseline. It compares your current exports to a previous release and tells you if your changes match the version bump you chose. If you removed a method but only bumped micro, it will call you out. Eclipse has API tooling that does the same inside the IDE.

Services and versions

OSGi services help tame dependencies. If you depend on an interface and find it in the registry, the package version still matters, but you do not need to bind to a particular bundle. Publish a service with a stable interface and let consumers import that package with a sane range. Blueprint and Declarative Services make this pattern readable and friendly.

// Declarative Services example
@Component
public class InvoiceServiceImpl implements InvoiceService {
  @Override
  public Invoice create(...) { ... }
}

// Exported package versioned at 1.2.0
// Consumers import com.example.billing.api;version="[1.2,2)"

A short checklist for healthy wiring

  • Version your exported packages with intent. Major for breaks, minor for new API, micro for fixes.
  • Import with ranges that end before the next major. Use bracket and parenthesis right.
  • Avoid Require Bundle unless you own both sides and accept the lock in.
  • Say no to split packages. Keep exports clean and internals private.
  • Use baseline tools to keep version bumps honest.
  • Test resolution in a real container. Felix or Equinox with an OBR or Karaf features file.

For managers and tech leads

This stuff reads like plumbing, yet it moves delivery dates. A sane versioning policy is a low cost way to prevent outages. Put it in writing. Make it part of code review to look at the export list and the version bump. Have a short document that states what your team guarantees to downstream users and for how long.

Automate checks. Run a baseline task in CI that fails the build when the bump does not match the API delta. Run an integration suite where bundles are resolved in an actual framework. A test that starts Felix, installs your features, and calls a few services beats a unit test that never touches the resolver.

Be careful with third party bundles. Keep a local catalog or OBR so you control which versions enter your app. Avoid qualifier builds in production. If you need to update a dependency, do it in a branch, adjust import ranges, and run through the wiring tests before merge. Treat package versions as contracts and you will sleep better near release dates.

Last point. Reward the habit of writing small clear APIs. Good versioning becomes easy when the surface is tidy. Teams that practice this ship more often and spend less time on scary rescue calls.

Your turn

Pick a bundle in your codebase and do this today:

  • List its public API packages. If any are exported without a version, fix that. Start at 1.0.0.
  • Scan for consumers and add import ranges like [1.x,2) where x matches what you compile against.
  • Run a baseline against the previous release tag. If you added or removed signatures, adjust the export versions and release notes.
  • Spin up a clean Felix or Equinox, drop only the bundles you need, and check the resolve step. Watch the console for uses conflicts and fix them by tightening imports or aligning providers.
# Quick Equinox smoke test
java -jar org.eclipse.osgi.jar -console

osgi> install file:com.example.billing-1.3.2.jar
Bundle id is 5
osgi> start 5
# If it fails, check wiring:
osgi> diag 5

# With bnd
bnd print com.example.billing-1.3.2.jar | grep "Export-Package\|Import-Package"

Small steps add up. Next time you pull a tiny patch on a quiet weekend, the resolver will nod and move on. Your coffee will stay warm. And your users will never know a thing happened behind the curtain.

Software Architecture Software Engineering

Post navigation

Previous post
Next post
  • Digital Experience (94)
    • Experience Strategy (19)
    • Experience-Driven Commerce (5)
    • Multi-Channel Experience (9)
    • Personalization & Targeting (21)
    • SEO & Performance (10)
  • Marketing Technologies (92)
    • Analytics & Measurement (14)
    • Content Management Systems (45)
    • Customer Data Platforms (4)
    • Digital Asset Management (8)
    • Marketing Automation (6)
    • MarTech Stack & Strategy (10)
    • Technology Buying & ROI (3)
  • Software Engineering (310)
    • Business of Software (20)
    • Code (30)
    • Development Practices (52)
    • Digital Transformation (21)
    • Engineering Management (25)
    • General Software (82)
    • Productivity & Workflow (30)
    • Software Architecture (85)
    • Technical Implementation (23)
  • 2025 (12)
  • 2024 (8)
  • 2023 (18)
  • 2022 (13)
  • 2021 (3)
  • 2020 (8)
  • 2019 (8)
  • 2018 (23)
  • 2017 (17)
  • 2016 (40)
  • 2015 (37)
  • 2014 (25)
  • 2013 (28)
  • 2012 (24)
  • 2011 (30)
  • 2010 (42)
  • 2009 (25)
  • 2008 (13)
  • 2007 (33)
  • 2006 (26)

Ab Testing Adobe Adobe Analytics Adobe Target AEM agile-methodologies Analytics architecture-patterns CDP CMS coding-practices content-marketing Content Supply Chain Conversion Optimization Core Web Vitals customer-education Customer Data Platform Customer Experience Customer Journey DAM Data Layer Data Unification documentation DXP Individualization java Martech metrics mobile-development Mobile First Multichannel Omnichannel Personalization product-strategy project-management Responsive Design Search Engine Optimization Segmentation seo spring Targeting Tracking user-experience User Journey web-development

©2025 CMO & CTO | WordPress Theme by SuperbThemes