diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/env/LeaseAwareVaultPropertySource.java b/spring-vault-core/src/main/java/org/springframework/vault/core/env/LeaseAwareVaultPropertySource.java
new file mode 100644
index 00000000..e69de29b
diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/lease/SecretLeaseContainer.java b/spring-vault-core/src/main/java/org/springframework/vault/core/lease/SecretLeaseContainer.java
new file mode 100644
index 00000000..f81663b8
--- /dev/null
+++ b/spring-vault-core/src/main/java/org/springframework/vault/core/lease/SecretLeaseContainer.java
@@ -0,0 +1,727 @@
+/*
+ * Copyright 2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.vault.core.lease;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+import java.util.concurrent.atomic.AtomicReference;
+
+import lombok.extern.apachecommons.CommonsLog;
+
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.scheduling.TaskScheduler;
+import org.springframework.scheduling.Trigger;
+import org.springframework.scheduling.TriggerContext;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import org.springframework.vault.VaultException;
+import org.springframework.vault.client.VaultResponses;
+import org.springframework.vault.core.RestOperationsCallback;
+import org.springframework.vault.core.VaultOperations;
+import org.springframework.vault.core.lease.domain.Lease;
+import org.springframework.vault.core.lease.domain.RequestedSecret;
+import org.springframework.vault.core.lease.domain.RequestedSecret.Mode;
+import org.springframework.vault.core.lease.event.LeaseErrorListener;
+import org.springframework.vault.core.lease.event.LeaseListener;
+import org.springframework.vault.support.VaultResponseSupport;
+import org.springframework.web.client.HttpStatusCodeException;
+import org.springframework.web.client.RestOperations;
+
+/**
+ * Event-based container to request secrets from Vault and renew the associated
+ * {@link Lease}. Secrets can be rotated, depending on the requested
+ * {@link RequestedSecret.Mode}.
+ *
+ * Usage example:
+ *
+ *
+ *
+SecretLeaseContainer container = new SecretLeaseContainer(vaultOperations,
+ taskScheduler);
+
+final RequestedSecret requestedSecret = container
+ .requestRotatingSecret("mysql/creds/my-role");
+container.addLeaseListener(new LeaseListenerAdapter() {
+ @Override
+ public void onLeaseEvent(LeaseEvent leaseEvent) {
+
+ if (requestedSecret == leaseEvent.getSource()) {
+
+ if (leaseEvent instanceof LeaseCreatedEvent) {
+
+ }
+
+ if (leaseEvent instanceof LeaseExpiredEvent) {
+
+ }
+ }
+ }
+});
+
+container.afterPropertiesSet();
+container.start(); // events are triggered after starting the container
+ *
+ *
+ *
+ * This container keeps track over {@link RequestedSecret}s and requests secrets upon
+ * {@link #start()}. Leases qualified for {@link Lease#isRenewable() renewal} are renewed
+ * by this container applying {@code minRenewalSeconds}/{@code expiryThresholdSeconds} on
+ * a {@link TaskScheduler background thread}.
+ *
+ * Requests for secrets can define either renewal or rotation. Renewable leases are
+ * renewed until expiry. Rotating secrets renew their associated lease until expiry and
+ * request new secrets after expiry. Vault requires active interaction from a caller side
+ * to determine a secret is expired. Vault does not send any events. Expired secrets
+ * events can dispatch later than the actual expiry.
+ *
+ * The container dispatches lease events to {@link LeaseListener} and
+ * {@link LeaseErrorListener}. Event notifications are dispatched either on the
+ * {@link #start() stating} {@link Thread} or worker threads used for background renewal.
+ *
+ * Instances are thread-safe once {@link #afterPropertiesSet() initialized.}
+ *
+ * @author Mark Paluch
+ * @see RequestedSecret
+ * @see SecretLeaseEventPublisher
+ * @see Lease
+ */
+@CommonsLog
+public class SecretLeaseContainer extends SecretLeaseEventPublisher
+ implements InitializingBean, DisposableBean {
+
+ private final static AtomicIntegerFieldUpdater UPDATER = AtomicIntegerFieldUpdater
+ .newUpdater(SecretLeaseContainer.class, "status");
+
+ private static final AtomicInteger poolId = new AtomicInteger();
+
+ private final static int STATUS_INITIAL = 0;
+ private final static int STATUS_STARTED = 1;
+ private final static int STATUS_DESTROYED = 2;
+
+ private final List requestedSecrets = new CopyOnWriteArrayList();
+
+ private final Map renewals = new ConcurrentHashMap();
+
+ private final VaultOperations operations;
+
+ private int minRenewalSeconds = 10;
+
+ private int expiryThresholdSeconds = 60;
+
+ private TaskScheduler taskScheduler;
+
+ private boolean manageTaskScheduler;
+
+ private volatile boolean initialized;
+
+ private volatile int status = STATUS_INITIAL;
+
+ /**
+ * Creates a new {@link SecretLeaseContainer} given {@link VaultOperations}.
+ *
+ * @param operations must not be {@literal null}.
+ */
+ public SecretLeaseContainer(VaultOperations operations) {
+
+ Assert.notNull(operations, "VaultOperations must not be null");
+
+ this.operations = operations;
+ }
+
+ /**
+ * Creates a new {@link SecretLeaseContainer} given {@link VaultOperations} and
+ * {@link TaskScheduler}.
+ *
+ * @param operations must not be {@literal null}.
+ * @param taskScheduler must not be {@literal null}.
+ */
+ public SecretLeaseContainer(VaultOperations operations, TaskScheduler taskScheduler) {
+
+ Assert.notNull(operations, "VaultOperations must not be null");
+ Assert.notNull(taskScheduler, "TaskScheduler must not be null");
+
+ this.operations = operations;
+ this.taskScheduler = taskScheduler;
+ }
+
+ /**
+ * Set the expiry threshold. {@link Lease} is renewed the given seconds before it
+ * expires.
+ *
+ * @param expiryThresholdSeconds number of seconds before {@link Lease} expiry.
+ */
+ public void setExpiryThresholdSeconds(int expiryThresholdSeconds) {
+ this.expiryThresholdSeconds = expiryThresholdSeconds;
+ }
+
+ /**
+ * Sets the amount of seconds that is at least required before renewing a lease.
+ * {@code minRenewalSeconds} prevents renewals to happen too often.
+ *
+ * @param minRenewalSeconds number of seconds that is at least required before
+ * renewing a {@link Lease}.
+ */
+ public void setMinRenewalSeconds(int minRenewalSeconds) {
+ this.minRenewalSeconds = minRenewalSeconds;
+ }
+
+ public int getMinRenewalSeconds() {
+ return minRenewalSeconds;
+ }
+
+ public int getExpiryThresholdSeconds() {
+ return expiryThresholdSeconds;
+ }
+
+ /**
+ * Sets the {@link TaskScheduler} to use for scheduling and execution of lease
+ * renewals.
+ *
+ * @param taskScheduler must not be {@literal null}.
+ */
+ public void setTaskScheduler(TaskScheduler taskScheduler) {
+
+ Assert.notNull(taskScheduler, "TaskScheduler must not be null");
+ this.taskScheduler = taskScheduler;
+ }
+
+ /**
+ * Request a renewable secret at {@code path}.
+ *
+ * @param path must not be {@literal null} or empty.
+ * @return the {@link RequestedSecret}.
+ */
+ public RequestedSecret requestRenewableSecret(String path) {
+
+ RequestedSecret requestedSecret = RequestedSecret.renewable(path);
+ addRequestedSecret(requestedSecret);
+ return requestedSecret;
+ }
+
+ /**
+ * Request a rotating secret at {@code path}.
+ *
+ * @param path must not be {@literal null} or empty.
+ * @return the {@link RequestedSecret}.
+ */
+ public RequestedSecret requestRotatingSecret(String path) {
+
+ RequestedSecret requestedSecret = RequestedSecret.rotating(path);
+ addRequestedSecret(requestedSecret);
+ return requestedSecret;
+ }
+
+ /**
+ * Add a {@link RequestedSecret}.
+ *
+ * @param requestedSecret must not be {@literal null}.
+ */
+ public void addRequestedSecret(RequestedSecret requestedSecret) {
+
+ Assert.notNull(requestedSecret, "RequestedSecret must not be null");
+
+ this.requestedSecrets.add(requestedSecret);
+
+ if (initialized) {
+
+ LeaseRenewalScheduler leaseRenewalScheduler = new LeaseRenewalScheduler(
+ this.taskScheduler);
+ this.renewals.put(requestedSecret, leaseRenewalScheduler);
+
+ if (this.status == STATUS_STARTED) {
+ start(requestedSecret, leaseRenewalScheduler);
+ }
+ }
+ }
+
+ /**
+ * Start the {@link SecretLeaseContainer}. Starting the container will initially
+ * obtain secrets and leases for the requested secrets. A started container publishes
+ * events through {@link LeaseListener}. Additional secrets can be requested at any
+ * time.
+ *
+ * Multiple {@link #start()} calls are synchronized to start the container only once.
+ * Container start requires {@link #afterPropertiesSet() initialization} and cannot be
+ * started once the container was {@link #destroy() destroyed}.
+ *
+ * @see #afterPropertiesSet()
+ * @see #stop()
+ */
+ public void start() {
+
+ Assert.state(this.initialized, "Container is not initialized");
+ Assert.state(this.status != STATUS_DESTROYED,
+ "Container is destroyed and cannot be started");
+
+ Map renewals = new HashMap(
+ this.renewals);
+
+ if (UPDATER.compareAndSet(this, STATUS_INITIAL, STATUS_STARTED)) {
+
+ for (Entry entry : renewals
+ .entrySet()) {
+ start(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+
+ private void start(RequestedSecret requestedSecret,
+ LeaseRenewalScheduler renewalScheduler) {
+
+ VaultResponseSupport